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

De Wiki Gla
Ir para navegação Ir para pesquisar
m
m
 
(42 revisões intermediárias pelo mesmo usuário não estão sendo mostradas)
Linha 11: Linha 11:
local nz = utils.nz
local nz = utils.nz
local parseFlags = utils.parseFlags
local parseFlags = utils.parseFlags
-- ===== Cache de metadados das skills para herança dos Subskills =====
local SkillMetaStore = {}
local function currentPageKey()
    local title = mw.title.getCurrentTitle()
    return title and title.fullText or "__page__"
end
local function metaBucket(char)
    local key = currentPageKey() .. "::" .. (trim(char or "") ~= "" and trim(char) or "__char__")
    if not SkillMetaStore[key] then
        SkillMetaStore[key] = { byIndex = {}, byName = {} }
    end
    return SkillMetaStore[key]
end
local function storeSkillMeta(char, index, name, data)
    if not data then return end
    local bucket = metaBucket(char)
    if index then
        local idx = tonumber(index) or index
        bucket.byIndex[idx] = data
    end
    if name and trim(name) ~= "" then
        bucket.byName[trim(name)] = data
    end
end
local function fetchSkillMeta(char, ref)
    if not ref then return nil end
    local bucket = metaBucket(char)
    local idxNum = tonumber(ref)
    if idxNum and bucket.byIndex[idxNum] then
        return bucket.byIndex[idxNum]
    end
    if bucket.byIndex[ref] then
        return bucket.byIndex[ref]
    end
    local refName = trim(ref)
    if refName ~= "" and bucket.byName[refName] then
        return bucket.byName[refName]
    end
    return nil
end
local function inheritSubFromMeta(char, sub)
    if type(sub) ~= "table" then return sub end
    local ref = sub.refM or sub.M or sub.m or sub.name or sub.n
    if not ref or trim(ref) == "" then
        return sub
    end
    local meta = fetchSkillMeta(char, ref)
    if not meta then
        return sub
    end
    local function pick(current, fallback)
        if current == false or current == 0 or current == "0" then
            return current
        end
        if current ~= nil and trim(tostring(current)) ~= "" then
            return current
        end
        return fallback
    end
    local hydrated = {}
    for k, v in pairs(sub) do
        hydrated[k] = v
    end
    hydrated.name = pick(sub.name or sub.n, meta.name)
    hydrated.icon = pick(sub.icon, meta.icon)
    hydrated.level = pick(sub.level, meta.level)
    hydrated.video = pick(sub.video, meta.video)
    hydrated.energy = pick(sub.energy, meta.energy)
    hydrated.powerpve = pick(sub.powerpve, meta.powerpve)
    hydrated.powerpvp = pick(sub.powerpvp, meta.powerpvp)
    hydrated.cooldown = pick(sub.cooldown, meta.cooldown)
    hydrated.desc = pick(sub.desc, meta.desc)
    hydrated.descPt = pick(sub.descPt, meta.descPt)
    hydrated.descEn = pick(sub.descEn, meta.descEn)
    hydrated.descEs = pick(sub.descEs, meta.descEs)
    hydrated.descPl = pick(sub.descPl, meta.descPl)
    -- PRESERVA weapon explicitamente (não herda de meta)
    if sub.weapon ~= nil then
        hydrated.weapon = sub.weapon
    end
    if sub.weaponPacked ~= nil then
        hydrated.weaponPacked = sub.weaponPacked
    end
    if type(sub.desc_i18n) == "table" then
        hydrated.desc_i18n = sub.desc_i18n
    else
        hydrated.desc_i18n = {
            pt = hydrated.descPt,
            en = hydrated.descEn,
            es = hydrated.descEs,
            pl = hydrated.descPl
        }
    end
    if type(sub.subs) == "table" then
        hydrated.subs = {}
        for i, nested in ipairs(sub.subs) do
            hydrated.subs[i] = inheritSubFromMeta(char, nested)
        end
    end
    return hydrated
end


-- ===== Herança automática de descrições =====
-- ===== Herança automática de descrições =====


-- Busca descrição em skills principais
local function findDescInSkills(data, skillName)
local function findDescInSkills(data, skillName)
     if not data or not data.skills then return nil end
     if not data or not data.skills then return nil end
Linha 24: Linha 137:
end
end


-- Busca descrição em qualquer subskills do módulo
local function findDescInAnySubskills(data, skillName)
local function findDescInAnySubskills(data, skillName)
     if not data or not data.skills then return nil end
     if not data or not data.skills then return nil end
Linha 38: Linha 150:
end
end


-- Busca descrição com herança automática
local function resolveDescWithInheritance(data, skillName, directDesc)
local function resolveDescWithInheritance(data, skillName, directDesc)
    -- 1. Se tem desc direta, usa ela
     if directDesc and type(directDesc) == "table" and next(directDesc) then
     if directDesc and type(directDesc) == "table" and next(directDesc) then
         return directDesc
         return directDesc
     end
     end
    -- 2. Busca em skills principais
     local fromSkills = findDescInSkills(data, skillName)
     local fromSkills = findDescInSkills(data, skillName)
     if fromSkills then
     if fromSkills then
         return fromSkills
         return fromSkills
     end
     end
    -- 3. Busca em outras subskills
     local fromSubskills = findDescInAnySubskills(data, skillName)
     local fromSubskills = findDescInAnySubskills(data, skillName)
     if fromSubskills then
     if fromSubskills then
         return fromSubskills
         return fromSubskills
     end
     end
     return nil
     return nil
end
end


-- ===== Weapon (gera JSON com atributos da arma para uso em {{Skill}}) =====
-- ===== Weapon =====
-- Retorna formato: icon~pve~pvp~cd~video~energy (separado por ~)
-- Isso evita problemas com JSON dentro de templates aninhados


function p.weapon(frame)
function p.weapon(frame)
     local a = safeArgs(frame)
     local a = safeArgs(frame)
     local obj = {
     local icon = trim(a.icon or "") ~= "" and a.icon or "Nada.png"
        icon = (trim(a.icon or "") ~= "" and a.icon or "Nada.png"),
    local pve = trim(a.powerpve or "")
        energy = nz(a.energy),
    local pvp = trim(a.powerpvp or "")
         powerpve = nz(a.powerpve),
    local cd = trim(a.cooldown or "")
         powerpvp = nz(a.powerpvp),
    local video = trim(a.video or "")
         cooldown = nz(a.cooldown),
    local energy = trim(a.energy or "")
         video = a.video or ""
    -- Formato: icon~pve~pvp~cd~video~energy
    return icon .. "~" .. pve .. "~" .. pvp .. "~" .. cd .. "~" .. video .. "~" .. energy
end
 
-- Helper para parsear formato de weapon (icon~pve~pvp~cd~video~energy)
local function parseWeaponString(str)
    if not str or trim(str) == "" then return nil end
    str = trim(str)
 
    -- Se começa com {, é JSON (formato antigo)
    if str:sub(1, 1) == "{" then
        local ok, data = pcall(mw.text.jsonDecode, str)
         if ok and type(data) == "table" then
            local result = {
                icon = data.icon or "Nada.png"
            }
            -- Só inclui campos que existem (não inclui nil)
            if data.powerpve ~= nil then result.powerpve = data.powerpve end
            if data.powerpvp ~= nil then result.powerpvp = data.powerpvp end
            if data.cooldown ~= nil then result.cooldown = data.cooldown end
            if data.energy ~= nil then result.energy = data.energy end
            if data.video and trim(data.video) ~= "" then result.video = data.video end
            return result
        end
        return nil
    end
 
    -- Formato novo: icon~pve~pvp~cd~video~energy
    -- IMPORTANTE: precisa preservar campos vazios para manter ordem fixa
    local parts = {}
    -- Split manual que preserva campos vazios
    local start = 1
    while true do
        local pos = str:find("~", start)
        if not pos then
            -- Último campo
            table.insert(parts, trim(str:sub(start)))
            break
        end
        table.insert(parts, trim(str:sub(start, pos - 1)))
         start = pos + 1
    end
    -- Garante que temos pelo menos 6 campos (preenche com "" se necessário)
    while #parts < 6 do
        table.insert(parts, "")
    end
    -- Também aceita formato com vírgula: pve,pvp,cd,video
    if #parts < 2 then
        parts = {}
        for part in str:gmatch("[^,]+") do
            table.insert(parts, trim(part))
        end
         if #parts >= 1 then
            local result = {
                icon = "Nada.png"
            }
            -- Só inclui campos que existem (não inclui nil)
            if nz(parts[1]) then result.powerpve = parts[1] end
            if nz(parts[2]) then result.powerpvp = parts[2] end
            if nz(parts[3]) then result.cooldown = parts[3] end
            if parts[4] and trim(parts[4]) ~= "" then result.video = parts[4] end
            if nz(parts[5]) then result.energy = parts[5] end
            return result
        end
        return nil
    end
 
    -- Garante que temos pelo menos as partes básicas (icon sempre existe)
    local result = {
         icon = nz(parts[1]) or "Nada.png"
     }
     }
     return mw.text.jsonEncode(obj)
     -- Só inclui campos que existem (não inclui nil)
    if nz(parts[2]) then result.powerpve = parts[2] end
    if nz(parts[3]) then result.powerpvp = parts[3] end
    if nz(parts[4]) then result.cooldown = parts[4] end
    if parts[5] and trim(parts[5]) ~= "" then result.video = parts[5] end
    if nz(parts[6]) then result.energy = parts[6] end
    return result
end
 
local function assignWeaponFromArg(obj, weaponStr)
    if not obj then return end
    local packed = trim(weaponStr or "")
    if packed ~= "" then
        -- Remove quebras de linha e espaços extras que podem vir da expansão do MediaWiki
        packed = packed:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
        obj.weaponPacked = packed
        local parsed = parseWeaponString(packed)
        if parsed then
            obj.weapon = parsed
        end
    end
end
 
-- Normaliza uma tabela weapon vinda do módulo do personagem
-- Aceita tanto weapon.desc quanto weapon.desc_i18n e sempre retorna weapon.desc_i18n
local function normalizeWeaponTable(raw)
    if type(raw) ~= "table" then
        return nil
    end
 
    local w = {}
    -- Só inclui campos que existem (não inclui nil)
    if raw.icon then w.icon = raw.icon else w.icon = "Nada.png" end
    if raw.powerpve ~= nil then w.powerpve = raw.powerpve end
    if raw.powerpvp ~= nil then w.powerpvp = raw.powerpvp end
    if raw.cooldown ~= nil then w.cooldown = raw.cooldown end
    if raw.energy ~= nil then w.energy = raw.energy end
    -- Só inclui video se não estiver vazio
    if raw.video and trim(raw.video) ~= "" then
        w.video = raw.video
    end
 
    -- Aceita tanto raw.desc_i18n quanto raw.desc
    local src = nil
    if type(raw.desc_i18n) == "table" then
        src = raw.desc_i18n
    elseif type(raw.desc) == "table" then
        src = raw.desc
    end
 
    if src then
        local desc_i18n = {}
        for _, code in ipairs({ "pt", "en", "es", "pl" }) do
            local d = src[code]
            if d and trim(d) ~= "" then
                -- Usa a função colorize já existente no módulo
                desc_i18n[code] = colorize(d)
            end
        end
        if next(desc_i18n) ~= nil then
            w.desc_i18n = desc_i18n
        end
    end
 
    return w
end
 
local function ensureWeaponHydrated(sub)
    if type(sub) ~= "table" then return end
    -- Se weapon já é uma tabela válida, normaliza para garantir desc_i18n
    if sub.weapon and type(sub.weapon) == "table" and next(sub.weapon) ~= nil then
        local normalized = normalizeWeaponTable(sub.weapon)
        if normalized then
            sub.weapon = normalized
        end
        return -- Já está hidratado e normalizado
    end
    -- Se weapon é string, tenta parsear
    if sub.weapon and type(sub.weapon) ~= "table" then
        local parsed = parseWeaponString(sub.weapon)
        if parsed and next(parsed) ~= nil then
            sub.weapon = parsed
        else
            -- Se parse falhou mas tem weaponPacked, tenta depois
            if not sub.weaponPacked then
                sub.weaponPacked = sub.weapon
            end
            sub.weapon = nil
        end
    end
    -- Se não tem weapon mas tem weaponPacked, tenta parsear
    if not sub.weapon and sub.weaponPacked and trim(sub.weaponPacked) ~= "" then
        local parsed = parseWeaponString(sub.weaponPacked)
        if parsed and next(parsed) ~= nil then
            sub.weapon = parsed
        end
    end
end
 
local function normalizeSubTree(sub)
    if type(sub) ~= "table" then return end
    -- Preserva weapon antes de hidratar
    local hadWeapon = sub.weapon ~= nil
    local weaponBefore = sub.weapon
    ensureWeaponHydrated(sub)
    -- Se tinha weapon mas perdeu após hidratação, tenta recuperar do weaponPacked
    if hadWeapon and not sub.weapon and sub.weaponPacked then
        local parsed = parseWeaponString(sub.weaponPacked)
        if parsed and next(parsed) ~= nil then
            sub.weapon = parsed
        end
    end
    -- Se tinha weapon como objeto válido antes, preserva mesmo se ensureWeaponHydrated removeu
    if weaponBefore and type(weaponBefore) == "table" and next(weaponBefore) ~= nil and not sub.weapon then
        sub.weapon = weaponBefore
    end
    if type(sub.subs) == "table" then
        for _, nested in ipairs(sub.subs) do
            normalizeSubTree(nested)
        end
    end
    -- Só remove weaponPacked se weapon está válido
    if sub.weapon and type(sub.weapon) == "table" and next(sub.weapon) ~= nil then
        sub.weaponPacked = nil
    end
end
end


Linha 80: Linha 380:
     local a = safeArgs(frame)
     local a = safeArgs(frame)
     local lang = trim((a.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower())
     local lang = trim((a.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower())
    -- Usa resolveCharFromFrames que busca recursivamente em todos os parent frames
     local char = resolveCharFromFrames(frame, a)
     local char = resolveCharFromFrames(frame, a)
     local data = requireCharacterModule(char) or {}
     local data = requireCharacterModule(char) or {}
Linha 105: Linha 404:


     local chosen = desc_i18n[lang] or desc_i18n["pt"] or ""
     local chosen = desc_i18n[lang] or desc_i18n["pt"] or ""
    -- Parse flags CSV
     local flagsArr = parseFlags(a.flags)
     local flagsArr = parseFlags(a.flags)


Linha 122: Linha 419:
         obj.flags = flagsArr
         obj.flags = flagsArr
     end
     end
     if chosen ~= "" then
     if chosen ~= "" then
         obj.desc = chosen
         obj.desc = chosen
Linha 134: Linha 430:
     end
     end


     -- empacota subskills vindas via |subs= (cada {{Subskill}} vira um objeto JSON)
     -- Registra metadados desta skill para herança futura
    if a.M and trim(a.M) ~= "" then
        local metaData = {
            name = obj.name or name or "",
            icon = obj.icon,
            level = obj.level,
            energy = obj.energy,
            powerpve = obj.powerpve,
            powerpvp = obj.powerpvp,
            cooldown = obj.cooldown,
            video = obj.video,
            desc = obj.desc,
            descPt = obj.descPt,
            descEn = obj.descEn,
            descEs = obj.descEs,
            descPl = obj.descPl
        }
        storeSkillMeta(char, tonumber(a.M) or a.M, metaData.name, metaData)
    end
 
    -- Subskills vindas via |subs=
     do
     do
         local subsPacked = a.subs or ""
         local subsPacked = a.subs or ""
         local subsArr = {}
         local subsArr = {}
         for _, chunk in ipairs(collectJsonObjects(subsPacked)) do
 
         -- Primeiro: extrai índices numéricos do início (antes dos JSONs)
        -- Formato: "1,2,3,4,5 {json1}{json2}" ou só "1,2,3,4,5"
        local indexPart = subsPacked:match("^([%s%d,]+)")
        local addedIndices = {} -- Track which indices were added
        if indexPart then
            for idx in indexPart:gmatch("(%d+)") do
                local i = tonumber(idx)
                if i and data and data.order and data.order[i] then
                    local skillName = data.order[i]
                    local skillData = (data.skills or {})[skillName] or {}
                    local subObj = {
                        refS = tostring(i),
                        name = skillName,
                        n = skillName,
                        icon = "", -- JS vai herdar da skill principal
                        level = "",
                        video = "",
                        energy = nil,
                        powerpve = nil,
                        powerpvp = nil,
                        cooldown = nil
                    }
                    -- Busca descrição do módulo
                    if type(skillData.desc) == "table" then
                        local desc_i18n = {}
                        local langs = { "pt", "en", "es", "pl" }
                        for _, code in ipairs(langs) do
                            local d = skillData.desc[code]
                            if d and trim(d) ~= "" then
                                desc_i18n[code] = colorize(d)
                            end
                        end
                        if next(desc_i18n) then
                            subObj.desc_i18n = desc_i18n
                            subObj.descPt = desc_i18n.pt
                            subObj.descEn = desc_i18n.en
                            subObj.descEs = desc_i18n.es
                            subObj.descPl = desc_i18n.pl
                        end
                    end
                    table.insert(subsArr, subObj)
                    addedIndices[tostring(i)] = #subsArr -- Store position
                end
            end
        end
 
        -- Depois: processa JSONs (das {{Subskill}} expandidas)
        local chunks = collectJsonObjects(subsPacked)
 
        -- Se um JSON tem refM que já foi adicionado via índice, substitui/merge
        for _, chunk in ipairs(chunks) do
             local ok, sk = pcall(mw.text.jsonDecode, chunk)
             local ok, sk = pcall(mw.text.jsonDecode, chunk)
             if ok and type(sk) == "table" then
             if ok and type(sk) == "table" then
                 table.insert(subsArr, sk)
                 ensureWeaponHydrated(sk)
                local ref = sk.refM or sk.M or sk.m or sk.refS or sk.S or sk.s
                if ref and addedIndices[tostring(ref)] then
                    -- Merge: atualiza o objeto existente com os atributos do JSON
                    local pos = addedIndices[tostring(ref)]
                    local existing = subsArr[pos]
                    -- Sobrescreve apenas atributos não vazios do JSON
                    if sk.video and trim(sk.video) ~= "" then existing.video = sk.video end
                    if sk.icon and trim(sk.icon) ~= "" then existing.icon = sk.icon end
                    if sk.level and trim(tostring(sk.level)) ~= "" then existing.level = sk.level end
                    if sk.energy ~= nil then existing.energy = sk.energy end
                    if sk.powerpve ~= nil then existing.powerpve = sk.powerpve end
                    if sk.powerpvp ~= nil then existing.powerpvp = sk.powerpvp end
                    if sk.cooldown ~= nil then existing.cooldown = sk.cooldown end
                    if sk.weapon ~= nil then existing.weapon = sk.weapon end
                    if sk.weaponPacked ~= nil then existing.weaponPacked = sk.weaponPacked end
 
                    -- BUSCA WEAPON DO MÓDULO LUA para fazer merge
                    -- Se weapon já existe mas não tem desc_i18n, busca do módulo para completar
                    local subName = existing.name or existing.n or ""
                    if subName ~= "" then
                        for _, skillKey in ipairs(data.order or {}) do
                            local skillData = (data.skills or {})[skillKey]
                            if type(skillData) == "table" and type(skillData.subskills) == "table" then
                                local moduleSub = skillData.subskills[subName]
                                if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then
                                    local moduleWeapon = normalizeWeaponTable(moduleSub.weapon)
                                    if moduleWeapon then
                                        if existing.weapon and type(existing.weapon) == "table" then
                                            -- Merge: preserva atributos do template, adiciona desc_i18n do módulo
                                            if moduleWeapon.desc_i18n and (not existing.weapon.desc_i18n) then
                                                existing.weapon.desc_i18n = moduleWeapon.desc_i18n
                                            end
                                            -- Se não tem icon no template, usa do módulo
                                            if (not existing.weapon.icon or existing.weapon.icon == "Nada.png") and moduleWeapon.icon then
                                                existing.weapon.icon = moduleWeapon.icon
                                            end
                                            -- Se não tem atributos no template, usa do módulo
                                            if existing.weapon.powerpve == nil and moduleWeapon.powerpve ~= nil then
                                                existing.weapon.powerpve = moduleWeapon.powerpve
                                            end
                                            if existing.weapon.powerpvp == nil and moduleWeapon.powerpvp ~= nil then
                                                existing.weapon.powerpvp = moduleWeapon.powerpvp
                                            end
                                            if existing.weapon.cooldown == nil and moduleWeapon.cooldown ~= nil then
                                                existing.weapon.cooldown = moduleWeapon.cooldown
                                            end
                                            if existing.weapon.energy == nil and moduleWeapon.energy ~= nil then
                                                existing.weapon.energy = moduleWeapon.energy
                                            end
                                            if (not existing.weapon.video or existing.weapon.video == "") and moduleWeapon.video then
                                                existing.weapon.video = moduleWeapon.video
                                            end
                                        else
                                            -- Se não tem weapon ainda, usa o do módulo
                                            existing.weapon = moduleWeapon
                                        end
                                        break
                                    end
                                end
                            end
                        end
                    end
 
                    -- Garante que weapon está hidratado após merge
                    ensureWeaponHydrated(existing)
                    -- Marca como já processado para não adicionar de novo
                    sk._merged = true
                end
             end
             end
         end
         end
         -- se não vier via |subs=, tenta obter do módulo (suborder/subskills)
         for _, chunk in ipairs(chunks) do
            local ok, sk = pcall(mw.text.jsonDecode, chunk)
            if ok and type(sk) == "table" then
                ensureWeaponHydrated(sk)
                -- Pula se já foi mergeado com um índice
                local ref = sk.refM or sk.M or sk.m or sk.refS or sk.S or sk.s
                local skipChunk = false
                if ref and addedIndices[tostring(ref)] then
                    -- Já foi processado no merge acima, pula
                    skipChunk = true
                end
 
                if not skipChunk then
                    -- Se tem ref (M ou S), aplica herança do cache OU busca diretamente
                    if ref and trim(tostring(ref)) ~= "" then
                        local mainMeta = fetchSkillMeta(char, ref)
 
                        -- Se não tem no cache, ref corresponde a M=N, então busca pelo índice
                        if not mainMeta then
                            local idx = tonumber(ref)
                            if idx and data and data.order and data.order[idx] then
                                local skillName = data.order[idx]
                                local skillData = (data.skills or {})[skillName] or {}
                                -- Cria um meta básico com dados do módulo
                                mainMeta = {
                                    name = skillName,
                                    icon = "", -- será preenchido pelo JS
                                    level = "",
                                    video = "",
                                    energy = nil,
                                    powerpve = nil,
                                    powerpvp = nil,
                                    cooldown = nil
                                }
                                -- Busca descrição
                                if type(skillData.desc) == "table" then
                                    local langs = { "pt", "en", "es", "pl" }
                                    for _, code in ipairs(langs) do
                                        local d = skillData.desc[code]
                                        if d and trim(d) ~= "" then
                                            mainMeta["desc" .. code:sub(1, 1):upper() .. code:sub(2)] = colorize(d)
                                        end
                                    end
                                end
                            end
                        end
 
                        if mainMeta then
                            -- Preenche nome se não tiver
                            if (not sk.name or trim(sk.name) == "") and (not sk.n or trim(sk.n) == "") then
                                sk.name = mainMeta.name
                                sk.n = mainMeta.name
                            end
                            -- Herda atributos vazios
                            if not sk.icon or trim(sk.icon) == "" then sk.icon = mainMeta.icon end
                            if not sk.level or trim(tostring(sk.level)) == "" then sk.level = mainMeta.level end
                            if not sk.video or trim(sk.video) == "" then sk.video = mainMeta.video end
                            if sk.energy == nil then sk.energy = mainMeta.energy end
                            if sk.powerpve == nil then sk.powerpve = mainMeta.powerpve end
                            if sk.powerpvp == nil then sk.powerpvp = mainMeta.powerpvp end
                            if sk.cooldown == nil then sk.cooldown = mainMeta.cooldown end
                            -- Herda descrição
                            if not sk.desc_i18n and not sk.descPt then
                                -- Busca descrição do módulo pelo nome
                                local foundDesc = resolveDescWithInheritance(data, mainMeta.name, nil)
                                if foundDesc then
                                    local langs = { "pt", "en", "es", "pl" }
                                    for _, code in ipairs(langs) do
                                        local d = foundDesc[code]
                                        if d and trim(d) ~= "" then
                                            sk["desc" .. code:sub(1, 1):upper() .. code:sub(2)] = colorize(d)
                                        end
                                    end
                                    sk.desc_i18n = {
                                        pt = sk.descPt,
                                        en = sk.descEn,
                                        es = sk.descEs,
                                        pl = sk.descPl
                                    }
                                elseif mainMeta.descPt or mainMeta.descEn then
                                    sk.descPt = mainMeta.descPt
                                    sk.descEn = mainMeta.descEn
                                    sk.descEs = mainMeta.descEs
                                    sk.descPl = mainMeta.descPl
                                    sk.desc_i18n = {
                                        pt = mainMeta.descPt,
                                        en = mainMeta.descEn,
                                        es = mainMeta.descEs,
                                        pl = mainMeta.descPl
                                    }
                                end
                            end
                        end
 
                        -- Mesmo sem mainMeta, mantém refM para o JS processar
                        if not mainMeta then
                            sk.refM = ref
                        end
                    end
                    -- Fallback: Se tem refM explícito no JSON, aplica herança do cache
                    local refMExplicit = sk.refM or sk.M or sk.m
                    if refMExplicit and trim(tostring(refMExplicit)) ~= "" and not ref then
                        local mainMeta = fetchSkillMeta(char, refMExplicit)
                        if mainMeta then
                            -- Preenche nome se não tiver
                            if (not sk.name or trim(sk.name) == "") and (not sk.n or trim(sk.n) == "") then
                                sk.name = mainMeta.name
                                sk.n = mainMeta.name
                            end
                            -- Herda atributos vazios
                            if not sk.icon or trim(sk.icon) == "" then sk.icon = mainMeta.icon end
                            if not sk.level or trim(tostring(sk.level)) == "" then sk.level = mainMeta.level end
                            if not sk.video or trim(sk.video) == "" then sk.video = mainMeta.video end
                            if sk.energy == nil then sk.energy = mainMeta.energy end
                            if sk.powerpve == nil then sk.powerpve = mainMeta.powerpve end
                            if sk.powerpvp == nil then sk.powerpvp = mainMeta.powerpvp end
                            if sk.cooldown == nil then sk.cooldown = mainMeta.cooldown end
                            -- Herda descrição
                            if not sk.desc_i18n and not sk.descPt then
                                sk.descPt = mainMeta.descPt
                                sk.descEn = mainMeta.descEn
                                sk.descEs = mainMeta.descEs
                                sk.descPl = mainMeta.descPl
                                sk.desc_i18n = {
                                    pt = mainMeta.descPt,
                                    en = mainMeta.descEn,
                                    es = mainMeta.descEs,
                                    pl = mainMeta.descPl
                                }
                            end
                        end
                    end
 
                    -- BUSCA WEAPON DO MÓDULO LUA para fazer merge
                    -- Se weapon já existe mas não tem desc_i18n, busca do módulo para completar
                    local subName = sk.name or sk.n or ""
                    if subName ~= "" then
                        for _, skillKey in ipairs(data.order or {}) do
                            local skillData = (data.skills or {})[skillKey]
                            if type(skillData) == "table" and type(skillData.subskills) == "table" then
                                local moduleSub = skillData.subskills[subName]
                                if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then
                                    local moduleWeapon = normalizeWeaponTable(moduleSub.weapon)
                                    if moduleWeapon then
                                        if sk.weapon and type(sk.weapon) == "table" then
                                            -- Merge: preserva atributos do template, adiciona desc_i18n do módulo
                                            if moduleWeapon.desc_i18n and (not sk.weapon.desc_i18n) then
                                                sk.weapon.desc_i18n = moduleWeapon.desc_i18n
                                            end
                                            -- Se não tem icon no template, usa do módulo
                                            if (not sk.weapon.icon or sk.weapon.icon == "Nada.png") and moduleWeapon.icon then
                                                sk.weapon.icon = moduleWeapon.icon
                                            end
                                            -- Se não tem atributos no template, usa do módulo
                                            if sk.weapon.powerpve == nil and moduleWeapon.powerpve ~= nil then
                                                sk.weapon.powerpve = moduleWeapon.powerpve
                                            end
                                            if sk.weapon.powerpvp == nil and moduleWeapon.powerpvp ~= nil then
                                                sk.weapon.powerpvp = moduleWeapon.powerpvp
                                            end
                                            if sk.weapon.cooldown == nil and moduleWeapon.cooldown ~= nil then
                                                sk.weapon.cooldown = moduleWeapon.cooldown
                                            end
                                            if sk.weapon.energy == nil and moduleWeapon.energy ~= nil then
                                                sk.weapon.energy = moduleWeapon.energy
                                            end
                                            if (not sk.weapon.video or sk.weapon.video == "") and moduleWeapon.video then
                                                sk.weapon.video = moduleWeapon.video
                                            end
                                        else
                                            -- Se não tem weapon ainda, usa o do módulo
                                            sk.weapon = moduleWeapon
                                        end
                                        break
                                    end
                                end
                            end
                        end
                    end
 
                    -- Garante que weapon está normalizado antes de adicionar
                    ensureWeaponHydrated(sk)
                    table.insert(subsArr, sk)
                end
            end -- end if not skipChunk
        end
        -- Fallback: subskills do módulo .lua
         if #subsArr == 0 and a.M and tonumber(a.M) then
         if #subsArr == 0 and a.M and tonumber(a.M) then
             local m = tonumber(a.M)
             local m = tonumber(a.M)
Linha 156: Linha 777:
                     local sub = subskills[n]
                     local sub = subskills[n]
                     if type(sub) == "table" then
                     if type(sub) == "table" then
                        -- HERANÇA: se sub não tem desc, busca automaticamente
                         local resolvedDesc = resolveDescWithInheritance(data, n, sub.desc)
                         local resolvedDesc = resolveDescWithInheritance(data, n, sub.desc)
                        -- Se tem inherit, busca atributos da skill principal referenciada
                        local inheritIdx = sub.inherit
                        local inheritedIcon, inheritedLevel, inheritedVideo = "", "", ""
                        local inheritedEnergy, inheritedPvE, inheritedPvP, inheritedCD = nil, nil, nil, nil
                        if inheritIdx and tonumber(inheritIdx) then
                            local mainSkillMeta = fetchSkillMeta(char, inheritIdx)
                            if mainSkillMeta then
                                inheritedIcon = mainSkillMeta.icon or ""
                                inheritedLevel = mainSkillMeta.level or ""
                                inheritedVideo = mainSkillMeta.video or ""
                                inheritedEnergy = mainSkillMeta.energy
                                inheritedPvE = mainSkillMeta.powerpve
                                inheritedPvP = mainSkillMeta.powerpvp
                                inheritedCD = mainSkillMeta.cooldown
                                -- Se não tem descrição própria, herda da skill principal
                                if not resolvedDesc then
                                    resolvedDesc = {
                                        pt = mainSkillMeta.descPt,
                                        en = mainSkillMeta.descEn,
                                        es = mainSkillMeta.descEs,
                                        pl = mainSkillMeta.descPl
                                    }
                                end
                            end
                        end
                        -- Coloriza as descrições
                        local desc_i18n = {}
                        if resolvedDesc then
                            local langs = { "pt", "en", "es", "pl" }
                            for _, code in ipairs(langs) do
                                local d = resolvedDesc[code]
                                if d and trim(d) ~= "" then
                                    desc_i18n[code] = colorize(d)
                                end
                            end
                        end


                         local subObj = {
                         local subObj = {
                             n = n,
                             n = n,
                             name = n,
                             name = n,
                             icon = sub.icon or "",
                             icon = (sub.icon and sub.icon ~= "") and sub.icon or inheritedIcon,
                             level = sub.level or "",
                             level = (sub.level and sub.level ~= "") and tostring(sub.level) or inheritedLevel,
                             video = sub.video or "",
                             video = (sub.video and sub.video ~= "") and sub.video or inheritedVideo,
                             energy = sub.energy,
                             energy = sub.energy or inheritedEnergy,
                             powerpve = sub.powerpve,
                             powerpve = sub.powerpve or inheritedPvE,
                             powerpvp = sub.powerpvp,
                             powerpvp = sub.powerpvp or inheritedPvP,
                             cooldown = sub.cooldown,
                             cooldown = sub.cooldown or inheritedCD,
                             desc_i18n = resolvedDesc,
                             desc_i18n = next(desc_i18n) and desc_i18n or nil,
                             descPt = resolvedDesc and resolvedDesc.pt or nil,
                             descPt = desc_i18n.pt or nil,
                             descEn = resolvedDesc and resolvedDesc.en or nil,
                             descEn = desc_i18n.en or nil,
                             descEs = resolvedDesc and resolvedDesc.es or nil,
                             descEs = desc_i18n.es or nil,
                             descPl = resolvedDesc and resolvedDesc.pl or nil
                             descPl = desc_i18n.pl or nil
                         }
                         }
                         -- nested subs do módulo (sub.suborder/sub.subskills)
                         -- INCLUI WEAPON do módulo Lua se existir
                        if type(sub.weapon) == "table" then
                            local weaponObj = normalizeWeaponTable(sub.weapon)
                            if weaponObj then
                                subObj.weapon = weaponObj
                            end
                        end
                         if type(sub.suborder) == "table" and type(sub.subskills) == "table" then
                         if type(sub.suborder) == "table" and type(sub.subskills) == "table" then
                             local nested = {}
                             local nested = {}
Linha 181: Linha 846:
                                 local s2 = sub.subskills[nn]
                                 local s2 = sub.subskills[nn]
                                 if type(s2) == "table" then
                                 if type(s2) == "table" then
                                    -- HERANÇA para nested também
                                     local nestedDesc = resolveDescWithInheritance(data, nn, s2.desc)
                                     local nestedDesc = resolveDescWithInheritance(data, nn, s2.desc)
 
                                     local nestedObj = {
                                     table.insert(nested, {
                                         n = nn,
                                         n = nn,
                                         name = nn,
                                         name = nn,
Linha 199: Linha 862:
                                         descEs = nestedDesc and nestedDesc.es or nil,
                                         descEs = nestedDesc and nestedDesc.es or nil,
                                         descPl = nestedDesc and nestedDesc.pl or nil
                                         descPl = nestedDesc and nestedDesc.pl or nil
                                     })
                                     }
                                    -- INCLUI WEAPON do nested se existir
                                    if type(s2.weapon) == "table" then
                                        local weaponObj = normalizeWeaponTable(s2.weapon)
                                        if weaponObj then
                                            nestedObj.weapon = weaponObj
                                        end
                                    end
                                    table.insert(nested, nestedObj)
                                 end
                                 end
                             end
                             end
Linha 210: Linha 881:
         end
         end
         if #subsArr > 0 then
         if #subsArr > 0 then
            for i, sub in ipairs(subsArr) do
                subsArr[i] = inheritSubFromMeta(char, sub)
                -- BUSCA WEAPON DO MÓDULO LUA se não tem weapon ainda (após herança)
                local hasWeapon = subsArr[i].weapon and (
                    (type(subsArr[i].weapon) == "table" and next(subsArr[i].weapon) ~= nil) or
                    (type(subsArr[i].weapon) == "string" and trim(subsArr[i].weapon) ~= "")
                )
                if not hasWeapon then
                    local subName = subsArr[i].name or subsArr[i].n or ""
                    if subName ~= "" then
                        for _, skillKey in ipairs(data.order or {}) do
                            local skillData = (data.skills or {})[skillKey]
                            if type(skillData) == "table" and type(skillData.subskills) == "table" then
                                local moduleSub = skillData.subskills[subName]
                                if moduleSub then
                                    -- DEBUG: Log temporário
                                    if subName == "Karmic Jishin" then
                                        -- Verifica se tem weapon
                                    end
                                end
                                if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then
                                    local weaponObj = {
                                        icon = moduleSub.weapon.icon or "Nada.png",
                                        energy = moduleSub.weapon.energy,
                                        powerpve = moduleSub.weapon.powerpve,
                                        powerpvp = moduleSub.weapon.powerpvp,
                                        cooldown = moduleSub.weapon.cooldown,
                                        video = moduleSub.weapon.video or ""
                                    }
                                    if type(moduleSub.weapon.desc) == "table" then
                                        local weaponDesc = {}
                                        for _, code in ipairs({ "pt", "en", "es", "pl" }) do
                                            local d = moduleSub.weapon.desc[code]
                                            if d and trim(d) ~= "" then
                                                weaponDesc[code] = colorize(d)
                                            end
                                        end
                                        if next(weaponDesc) then
                                            weaponObj.desc_i18n = weaponDesc
                                        end
                                    end
                                    subsArr[i].weapon = weaponObj
                                    -- DEBUG: Log para confirmar que encontrou
                                    if subName == "Karmic Jishin" then
                                        -- Log temporário
                                    end
                                    break
                                end
                            end
                        end
                    end
                end
                normalizeSubTree(subsArr[i])
                -- VERIFICAÇÃO FINAL: Garante que weapon está presente e completo antes de serializar
                -- Busca do módulo para fazer merge (mesmo se já tem weapon do template)
                local subName = subsArr[i].name or subsArr[i].n or ""
                if subName ~= "" then
                    for _, skillKey in ipairs(data.order or {}) do
                        local skillData = (data.skills or {})[skillKey]
                        if type(skillData) == "table" and type(skillData.subskills) == "table" then
                            local moduleSub = skillData.subskills[subName]
                            if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then
                                local moduleWeapon = normalizeWeaponTable(moduleSub.weapon)
                                if moduleWeapon then
                                    if subsArr[i].weapon and type(subsArr[i].weapon) == "table" then
                                        -- Merge: preserva atributos do template, adiciona desc_i18n do módulo
                                        if moduleWeapon.desc_i18n and (not subsArr[i].weapon.desc_i18n) then
                                            subsArr[i].weapon.desc_i18n = moduleWeapon.desc_i18n
                                        end
                                        -- Se não tem icon no template, usa do módulo
                                        if (not subsArr[i].weapon.icon or subsArr[i].weapon.icon == "Nada.png") and moduleWeapon.icon then
                                            subsArr[i].weapon.icon = moduleWeapon.icon
                                        end
                                        -- Se não tem atributos no template, usa do módulo
                                        if subsArr[i].weapon.powerpve == nil and moduleWeapon.powerpve ~= nil then
                                            subsArr[i].weapon.powerpve = moduleWeapon.powerpve
                                        end
                                        if subsArr[i].weapon.powerpvp == nil and moduleWeapon.powerpvp ~= nil then
                                            subsArr[i].weapon.powerpvp = moduleWeapon.powerpvp
                                        end
                                        if subsArr[i].weapon.cooldown == nil and moduleWeapon.cooldown ~= nil then
                                            subsArr[i].weapon.cooldown = moduleWeapon.cooldown
                                        end
                                        if subsArr[i].weapon.energy == nil and moduleWeapon.energy ~= nil then
                                            subsArr[i].weapon.energy = moduleWeapon.energy
                                        end
                                        if (not subsArr[i].weapon.video or subsArr[i].weapon.video == "") and moduleWeapon.video then
                                            subsArr[i].weapon.video = moduleWeapon.video
                                        end
                                    else
                                        -- Se não tem weapon ainda, usa o do módulo
                                        subsArr[i].weapon = moduleWeapon
                                    end
                                    break
                                end
                            end
                        end
                    end
                end
            end
             obj.subs = subsArr
             obj.subs = subsArr
            -- SISTEMA UNIFICADO DE WEAPON PARA SUBSKILLS: Garante weapon normalizado e completo antes de serializar
            if type(obj.subs) == "table" then
                for i, sub in ipairs(obj.subs) do
                    local subName = sub.name or sub.n or ""
                    -- PASSO 1: Normaliza weapon existente
                    ensureWeaponHydrated(sub)
                    normalizeSubTree(sub)
                    -- PASSO 2: Se weapon ainda é string, tenta parsear
                    if sub.weapon and type(sub.weapon) ~= "table" then
                        local parsed = parseWeaponString(sub.weapon)
                        if parsed and next(parsed) ~= nil then
                            sub.weapon = parsed
                        else
                            sub.weapon = nil
                        end
                    end
                    -- PASSO 3: Busca weapon do módulo (sempre, para garantir merge completo)
                    if subName ~= "" then
                        for _, skillKey in ipairs(data.order or {}) do
                            local skillData = (data.skills or {})[skillKey]
                            if type(skillData) == "table" and type(skillData.subskills) == "table" then
                                -- Tenta match exato primeiro
                                local moduleSub = skillData.subskills[subName]
                                -- Se não encontrou, tenta case-insensitive
                                if not moduleSub then
                                    for k, v in pairs(skillData.subskills) do
                                        if type(k) == "string" and k:lower() == subName:lower() then
                                            moduleSub = v
                                            break
                                        end
                                    end
                                end
                                -- Se encontrou subskill no módulo com weapon, faz merge completo
                                if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then
                                    local moduleWeapon = normalizeWeaponTable(moduleSub.weapon)
                                    if moduleWeapon then
                                        if obj.subs[i].weapon and type(obj.subs[i].weapon) == "table" then
                                            -- MERGE: Combina weapon do template com weapon do módulo
                                            -- Descrição do módulo sempre tem prioridade (tem desc_i18n completo)
                                            if moduleWeapon.desc_i18n then
                                                obj.subs[i].weapon.desc_i18n = moduleWeapon.desc_i18n
                                            end
                                            -- Atributos: usa do módulo se não tem no template
                                            if not obj.subs[i].weapon.icon or obj.subs[i].weapon.icon == "Nada.png" then
                                                obj.subs[i].weapon.icon = moduleWeapon.icon
                                            end
                                            if obj.subs[i].weapon.powerpve == nil then
                                                obj.subs[i].weapon.powerpve = moduleWeapon.powerpve
                                            end
                                            if obj.subs[i].weapon.powerpvp == nil then
                                                obj.subs[i].weapon.powerpvp = moduleWeapon.powerpvp
                                            end
                                            if obj.subs[i].weapon.cooldown == nil then
                                                obj.subs[i].weapon.cooldown = moduleWeapon.cooldown
                                            end
                                            if obj.subs[i].weapon.energy == nil then
                                                obj.subs[i].weapon.energy = moduleWeapon.energy
                                            end
                                            if not obj.subs[i].weapon.video or obj.subs[i].weapon.video == "" then
                                                obj.subs[i].weapon.video = moduleWeapon.video
                                            end
                                        else
                                            -- Se não tem weapon ainda, usa o do módulo completo
                                            obj.subs[i].weapon = moduleWeapon
                                        end
                                        break
                                    end
                                end
                            end
                        end
                    end
                    -- PASSO 4: Garante que weapon está normalizado e válido
                    if obj.subs[i].weapon then
                        ensureWeaponHydrated(obj.subs[i])
                        -- Remove weaponPacked se weapon está válido
                        if type(obj.subs[i].weapon) == "table" and next(obj.subs[i].weapon) ~= nil then
                            obj.subs[i].weaponPacked = nil
                            -- Garante que desc_i18n existe se tem descrição
                            if obj.subs[i].weapon.desc_i18n and type(obj.subs[i].weapon.desc_i18n) == "table" then
                                -- Já está normalizado
                            elseif obj.subs[i].weapon.desc and type(obj.subs[i].weapon.desc) == "table" then
                                -- Converte desc para desc_i18n
                                local desc_i18n = {}
                                for _, code in ipairs({ "pt", "en", "es", "pl" }) do
                                    local d = obj.subs[i].weapon.desc[code]
                                    if d and trim(d) ~= "" then
                                        desc_i18n[code] = colorize(d)
                                    end
                                end
                                if next(desc_i18n) ~= nil then
                                    obj.subs[i].weapon.desc_i18n = desc_i18n
                                end
                            end
                        else
                            obj.subs[i].weapon = nil
                        end
                    end
                end
            end
         end
         end


         -- suborder do módulo (se existir pra esta skill M)
         -- suborder do módulo
         if a.M and tonumber(a.M) then
         if a.M and tonumber(a.M) then
             local m = tonumber(a.M)
             local m = tonumber(a.M)
Linha 225: Linha 1 100:
     end
     end


     -- ===== Weapon: parsear |weapon= e buscar desc do módulo =====
     -- Weapon
     do
     do
         local weaponPacked = trim(a.weapon or "")
         assignWeaponFromArg(obj, a.weapon)
         if weaponPacked ~= "" then
         if obj.weapon then
             -- Tentar parsear o JSON do {{Weapon}}
             if a.M and tonumber(a.M) then
            local ok, weaponData = pcall(mw.text.jsonDecode, weaponPacked)
                local m = tonumber(a.M)
            if ok and type(weaponData) == "table" then
                local order = (data and data.order) or {}
                -- Buscar descrição da arma no módulo (skills[key].weapon.desc)
                local key = order[m] or ""
                local weaponDesc_i18n = {}
                local sk = (data and data.skills and data.skills[key]) or {}
                if a.M and tonumber(a.M) then
                if type(sk) == "table" and type(sk.weapon) == "table" then
                    local m = tonumber(a.M)
                    local normalized = normalizeWeaponTable(sk.weapon)
                    local order = (data and data.order) or {}
                    if normalized then
                    local key = order[m] or ""
                        -- Preserva campos já existentes em obj.weapon se houver
                    local sk = (data and data.skills and data.skills[key]) or {}
                        if obj.weapon then
                    if type(sk) == "table" and type(sk.weapon) == "table" and type(sk.weapon.desc) == "table" then
                            -- Merge inteligente: só sobrescreve se o campo do módulo não estiver vazio
                        local langs = { "pt", "en", "es", "pl" }
                            -- E só sobrescreve se o campo do template estiver vazio ou não existir
                        for _, code in ipairs(langs) do
                            for k, v in pairs(normalized) do
                            local d = sk.weapon.desc[code]
                                -- Para video: SEMPRE preserva o do template se existir (não faz merge)
                            if d and trim(d) ~= "" then
                                if k == "video" then
                                weaponDesc_i18n[code] = colorize(d)
                                    -- Se o template tem vídeo, preserva (não sobrescreve com módulo)
                                    if obj.weapon.video and trim(obj.weapon.video) ~= "" then
                                        -- Mantém o vídeo do template, não faz nada
                                    elseif v and v ~= "" then
                                        -- Só usa do módulo se template não tem
                                        obj.weapon.video = v
                                    end
                                    -- Para icon: preserva o do template se não for Nada.png
                                elseif k == "icon" then
                                    if (not obj.weapon.icon or obj.weapon.icon == "" or obj.weapon.icon == "Nada.png") and v and v ~= "" and v ~= "Nada.png" then
                                        obj.weapon.icon = v
                                    end
                                    -- Para outros campos: só faz merge se o campo NÃO EXISTE no template (nil)
                                    -- Se o campo existe no template (mesmo que vazio), preserva o do template
                                else
                                    -- IMPORTANTE: só faz merge se o campo é nil (não existe), não se está vazio
                                    if obj.weapon[k] == nil and v ~= nil and v ~= "" then
                                        obj.weapon[k] = v
                                    end
                                end
                            end
                            -- Sempre adiciona desc_i18n do módulo se não tiver no template
                            if normalized.desc_i18n and not obj.weapon.desc_i18n then
                                obj.weapon.desc_i18n = normalized.desc_i18n
                             end
                             end
                        else
                            obj.weapon = normalized
                        end
                        -- Adiciona campos descPt, descEn, etc. para compatibilidade
                        if obj.weapon.desc_i18n then
                            obj.weapon.descPt = obj.weapon.desc_i18n.pt
                            obj.weapon.descEn = obj.weapon.desc_i18n.en
                            obj.weapon.descEs = obj.weapon.desc_i18n.es
                            obj.weapon.descPl = obj.weapon.desc_i18n.pl
                         end
                         end
                     end
                     end
                 end
                 end
                -- Montar objeto weapon com atributos do template + desc do módulo
                local weaponObj = {
                    icon = weaponData.icon or "Nada.png",
                    energy = weaponData.energy,
                    powerpve = weaponData.powerpve,
                    powerpvp = weaponData.powerpvp,
                    cooldown = weaponData.cooldown,
                    video = weaponData.video or ""
                }
                if next(weaponDesc_i18n) ~= nil then
                    weaponObj.desc_i18n = weaponDesc_i18n
                    weaponObj.descPt = weaponDesc_i18n.pt
                    weaponObj.descEn = weaponDesc_i18n.en
                    weaponObj.descEs = weaponDesc_i18n.es
                    weaponObj.descPl = weaponDesc_i18n.pl
                end
                obj.weapon = weaponObj
             end
             end
         end
         end
Linha 275: Linha 1 163:
end
end


-- ===== Emote (sem M) =====
-- ===== Emote =====


function p.emote(frame)
function p.emote(frame)
Linha 289: Linha 1 177:
end
end


-- ===== Subskill (nome n EXATO do suborder; agrega desc_i18n do módulo + atributos locais) =====
-- ===== Subskill =====
-- Gera JSON simples. Herança COMPLETA de atributos é feita no JavaScript.
-- Se |M= for passado sem |n=, retorna apenas refM para o JS resolver.


function p.subskill(frame)
function p.subskill(frame)
     local a = safeArgs(frame)
     local a = safeArgs(frame)
     local lang = trim((a.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower())
     local lang = trim((a.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower())
     local n = trim(a.n or "")
     local n = trim(a.n or "")
    local refM = trim(a.m or a.M or "")
    local refS = trim(a.S or a.s or "")
    -- Prioriza |M= sobre |S= para herança
    local ref = (refM ~= "" and refM) or refS
    local char = resolveCharFromFrames(frame, a)
    local data = requireCharacterModule(char) or {}
    -- Se tem |M= ou |S=, busca do cache e herda atributos
    if ref ~= "" then
        local meta = fetchSkillMeta(char, ref)
        if meta then
            -- Herda atributos da skill referenciada, sobrescreve só o que foi passado
            local function pick(explicit, inherited)
                if explicit ~= nil and trim(tostring(explicit)) ~= "" then
                    return explicit
                end
                return inherited
            end
            -- Busca descrição: primeiro do módulo pelo nome herdado, depois usa a da skill
            local inheritedName = meta.name or ""
            local desc_i18n = {}
            local foundDesc = resolveDescWithInheritance(data, inheritedName, nil)
            if foundDesc then
                local langs = { "pt", "en", "es", "pl" }
                for _, code in ipairs(langs) do
                    local d = foundDesc[code]
                    if d and trim(d) ~= "" then
                        desc_i18n[code] = colorize(d)
                    end
                end
            end
            -- Se não achou no módulo, usa descrição da skill original
            if not next(desc_i18n) then
                desc_i18n = {
                    pt = meta.descPt,
                    en = meta.descEn,
                    es = meta.descEs,
                    pl = meta.descPl
                }
            end
            local obj = {
                name = pick(a.n, inheritedName),
                icon = pick(a.icon, meta.icon),
                level = pick(a.level, meta.level),
                video = pick(a.video, meta.video),
                energy = nz(a.energy) or meta.energy,
                powerpve = nz(a.powerpve) or meta.powerpve,
                powerpvp = nz(a.powerpvp) or meta.powerpvp,
                cooldown = nz(a.cooldown) or meta.cooldown,
                back = (trim(a.back or "") == "yes" or trim(a.back or "") == "true") and true or nil,
                refM = ref
            }
            local flagsArr = parseFlags(a.flags)
            if #flagsArr > 0 then
                obj.flags = flagsArr
            end
            if next(desc_i18n) then
                obj.desc_i18n = desc_i18n
                obj.descPt = desc_i18n.pt
                obj.descEn = desc_i18n.en
                obj.descEs = desc_i18n.es
                obj.descPl = desc_i18n.pl
            end
            -- Weapon para subskill
            assignWeaponFromArg(obj, a.weapon)
            return mw.text.jsonEncode(obj)
        else
            -- Cache vazio - busca nome e descrição diretamente do módulo
            -- ref corresponde ao índice M
            local idx = tonumber(ref)
            local skillName = ""
            local desc_i18n = {}
            if idx and data and data.order and data.order[idx] then
                skillName = data.order[idx]
                local skillData = (data.skills or {})[skillName] or {}
                if type(skillData.desc) == "table" then
                    local langs = { "pt", "en", "es", "pl" }
                    for _, code in ipairs(langs) do
                        local d = skillData.desc[code]
                        if d and trim(d) ~= "" then
                            desc_i18n[code] = colorize(d)
                        end
                    end
                end
            end
            local obj = {
                refM = ref,
                name = skillName,
                n = skillName,
                icon = trim(a.icon or ""),
                level = trim(a.level or ""),
                video = trim(a.video or ""),
                energy = nz(a.energy),
                powerpve = nz(a.powerpve),
                powerpvp = nz(a.powerpvp),
                cooldown = nz(a.cooldown),
                back = (trim(a.back or "") == "yes" or trim(a.back or "") == "true") and true or nil
            }
            if trim(a.n or "") ~= "" then
                obj.name = trim(a.n)
                obj.n = trim(a.n)
            end
            if next(desc_i18n) then
                obj.desc_i18n = desc_i18n
                obj.descPt = desc_i18n.pt
                obj.descEn = desc_i18n.en
                obj.descEs = desc_i18n.es
                obj.descPl = desc_i18n.pl
            end
            local flagsArr = parseFlags(a.flags)
            if #flagsArr > 0 then
                obj.flags = flagsArr
            end
            -- Weapon para subskill
            assignWeaponFromArg(obj, a.weapon)
            return mw.text.jsonEncode(obj)
        end
    end
    -- Se só tem refM (sem nome), retorna objeto mínimo para o JS fazer herança completa
    if n == "" and refM ~= "" then
        local obj = {
            refM = refM,
            icon = trim(a.icon or ""),
            level = trim(a.level or ""),
            video = trim(a.video or ""),
            energy = nz(a.energy),
            powerpve = nz(a.powerpve),
            powerpvp = nz(a.powerpvp),
            cooldown = nz(a.cooldown),
            back = (trim(a.back or "") == "yes" or trim(a.back or "") == "true") and true or nil
        }
        local flagsArr = parseFlags(a.flags)
        if #flagsArr > 0 then
            obj.flags = flagsArr
        end
        -- Weapon para subskill
        assignWeaponFromArg(obj, a.weapon)
        return mw.text.jsonEncode(obj)
    end
    -- Se não tem nome nem refM nem refS, retorna vazio
     if n == "" then
     if n == "" then
         return mw.text.jsonEncode({})
         return mw.text.jsonEncode({})
     end
     end


     -- Usa resolveCharFromFrames que busca recursivamente em todos os parent frames
     -- Busca descrição do módulo .lua
    local char = resolveCharFromFrames(frame, a)
    local data = requireCharacterModule(char) or {}
 
    -- HERANÇA AUTOMÁTICA: busca desc com fallback
     local desc_i18n = {}
     local desc_i18n = {}
     local foundDesc = nil
     local foundDesc = nil
    -- Primeiro tenta encontrar em subskills (comportamento original)
     for _, key in ipairs(data.order or {}) do
     for _, key in ipairs(data.order or {}) do
         local sk = (data.skills or {})[key]
         local sk = (data.skills or {})[key]
Linha 318: Linha 1 354:
         end
         end
     end
     end
    -- Se não encontrou, aplica herança automática
     if not foundDesc then
     if not foundDesc then
         foundDesc = resolveDescWithInheritance(data, n, nil)
         foundDesc = resolveDescWithInheritance(data, n, nil)
     end
     end
    -- Coloriza as descrições encontradas
     if foundDesc then
     if foundDesc then
         local langs = { "pt", "en", "es", "pl" }
         local langs = { "pt", "en", "es", "pl" }
Linha 335: Linha 1 367:
     end
     end


    -- Parse flags CSV
     local flagsArr = parseFlags(a.flags)
     local flagsArr = parseFlags(a.flags)


     -- Atributos: valores do .wiki ou vazios (herança de atributos é feita no JavaScript)
     -- Objeto com nome explícito
    local refM = trim(a.m or a.M or "")
 
     local obj = {
     local obj = {
         name = n,
         name = n,
         icon = (trim(a.icon or "") ~= "" and a.icon or "Nada.png"),
         icon = trim(a.icon or ""),
         level = (trim(a.level or "") ~= "" and a.level or ""),
         level = trim(a.level or ""),
         video = a.video or "",
         video = trim(a.video or ""),
         energy = nz(a.energy),
         energy = nz(a.energy),
         powerpve = nz(a.powerpve),
         powerpve = nz(a.powerpve),
Linha 356: Linha 1 385:
         obj.refM = refM
         obj.refM = refM
     end
     end
     if #flagsArr > 0 then
     if #flagsArr > 0 then
         obj.flags = flagsArr
         obj.flags = flagsArr
     end
     end
     if next(desc_i18n) ~= nil then
     if next(desc_i18n) ~= nil then
         obj.desc_i18n = desc_i18n
         obj.desc_i18n = desc_i18n
Linha 369: Linha 1 396:
     end
     end


     -- Nested subs support: allow |subs= inside a subskill
     -- Nested subs
     do
     do
         local subsPacked = a.subs or ""
         local subsPacked = a.subs or ""
Linha 376: Linha 1 403:
             local ok, sub = pcall(mw.text.jsonDecode, chunk)
             local ok, sub = pcall(mw.text.jsonDecode, chunk)
             if ok and type(sub) == "table" then
             if ok and type(sub) == "table" then
                ensureWeaponHydrated(sub)
                 table.insert(subsArr, sub)
                 table.insert(subsArr, sub)
             end
             end
         end
         end
         if #subsArr > 0 then
         if #subsArr > 0 then
            for _, sub in ipairs(subsArr) do
                -- BUSCA WEAPON DO MÓDULO LUA se não tem weapon ainda (para nested subs também)
                local hasWeapon = sub.weapon and (
                    (type(sub.weapon) == "table" and next(sub.weapon) ~= nil) or
                    (type(sub.weapon) == "string" and trim(sub.weapon) ~= "")
                )
                if not hasWeapon then
                    local subName = sub.name or sub.n or ""
                    if subName ~= "" then
                        for _, skillKey in ipairs(data.order or {}) do
                            local skillData = (data.skills or {})[skillKey]
                            if type(skillData) == "table" and type(skillData.subskills) == "table" then
                                local moduleSub = skillData.subskills[subName]
                                if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then
                                    local weaponObj = normalizeWeaponTable(moduleSub.weapon)
                                    if weaponObj then
                                        sub.weapon = weaponObj
                                        break
                                    end
                                end
                            end
                        end
                    end
                end
                normalizeSubTree(sub)
            end
             obj.subs = subsArr
             obj.subs = subsArr
        end
    end
    -- Weapon para subskill
    -- Usa {{Weapon|...}} que retorna formato: icon~pve~pvp~cd~video~energy
    do
        assignWeaponFromArg(obj, a.weapon)
        -- Busca descrição do weapon no módulo Lua (subskills)
        for _, key in ipairs(data.order or {}) do
            local sk = (data.skills or {})[key]
            if type(sk) == "table" and type(sk.subskills) == "table" then
                local sub = sk.subskills[n]
                if sub and type(sub.weapon) == "table" then
                    local normalized = normalizeWeaponTable(sub.weapon)
                    if normalized then
                        -- Preserva campos já existentes em obj.weapon se houver
                        if obj.weapon then
                            -- Merge inteligente: só sobrescreve se o campo do módulo não estiver vazio
                            -- E só sobrescreve se o campo do template estiver vazio ou não existir
                            for k, v in pairs(normalized) do
                                -- Para video: SEMPRE preserva o do template se existir (não faz merge)
                                if k == "video" then
                                    -- Se o template tem vídeo, preserva (não sobrescreve com módulo)
                                    if obj.weapon.video and trim(obj.weapon.video) ~= "" then
                                        -- Mantém o vídeo do template, não faz nada
                                    elseif v and v ~= "" then
                                        -- Só usa do módulo se template não tem
                                        obj.weapon.video = v
                                    end
                                    -- Para icon: preserva o do template se não for Nada.png
                                elseif k == "icon" then
                                    if (not obj.weapon.icon or obj.weapon.icon == "" or obj.weapon.icon == "Nada.png") and v and v ~= "" and v ~= "Nada.png" then
                                        obj.weapon.icon = v
                                    end
                                    -- Para outros campos: só faz merge se o campo NÃO EXISTE no template (nil)
                                    -- Se o campo existe no template (mesmo que vazio), preserva o do template
                                else
                                    -- IMPORTANTE: só faz merge se o campo é nil (não existe), não se está vazio
                                    if obj.weapon[k] == nil and v ~= nil and v ~= "" then
                                        obj.weapon[k] = v
                                    end
                                end
                            end
                            -- Sempre adiciona desc_i18n do módulo se não tiver no template
                            if normalized.desc_i18n and not obj.weapon.desc_i18n then
                                obj.weapon.desc_i18n = normalized.desc_i18n
                            end
                        else
                            obj.weapon = normalized
                        end
                        -- Adiciona campos descPt, descEn, etc. para compatibilidade
                        if obj.weapon.desc_i18n then
                            obj.weapon.descPt = obj.weapon.desc_i18n.pt
                            obj.weapon.descEn = obj.weapon.desc_i18n.en
                            obj.weapon.descEs = obj.weapon.desc_i18n.es
                            obj.weapon.descPl = obj.weapon.desc_i18n.pl
                        end
                    end
                    break
                end
            end
        end
        if obj.weapon and type(obj.weapon) == "table" and next(obj.weapon) == nil then
            obj.weapon = nil
        end
    end
    -- GARANTIA FINAL: Normaliza weapon antes de serializar
    if obj.weapon then
        ensureWeaponHydrated(obj)
        -- Se weapon ainda é string ou inválido, tenta parsear
        if obj.weapon and type(obj.weapon) ~= "table" then
            local parsed = parseWeaponString(obj.weapon)
            if parsed and next(parsed) ~= nil then
                obj.weapon = parsed
            else
                obj.weapon = nil
            end
        end
        -- Remove weapon se estiver vazio
        if obj.weapon and type(obj.weapon) == "table" and next(obj.weapon) == nil then
            obj.weapon = nil
         end
         end
     end
     end

Edição atual tal como às 17h47min de 5 de dezembro de 2025

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

-- Módulo:Info.Skills — skill(), subskill(), emote()
local p = {}

local utils = require("Módulo:Info.Utils")
local trim = utils.trim
local safeArgs = utils.safeArgs
local collectJsonObjects = utils.collectJsonObjects
local requireCharacterModule = utils.requireCharacterModule
local resolveCharFromFrames = utils.resolveCharFromFrames
local colorize = utils.colorize
local nz = utils.nz
local parseFlags = utils.parseFlags

-- ===== Cache de metadados das skills para herança dos Subskills =====
local SkillMetaStore = {}

local function currentPageKey()
    local title = mw.title.getCurrentTitle()
    return title and title.fullText or "__page__"
end

local function metaBucket(char)
    local key = currentPageKey() .. "::" .. (trim(char or "") ~= "" and trim(char) or "__char__")
    if not SkillMetaStore[key] then
        SkillMetaStore[key] = { byIndex = {}, byName = {} }
    end
    return SkillMetaStore[key]
end

local function storeSkillMeta(char, index, name, data)
    if not data then return end
    local bucket = metaBucket(char)
    if index then
        local idx = tonumber(index) or index
        bucket.byIndex[idx] = data
    end
    if name and trim(name) ~= "" then
        bucket.byName[trim(name)] = data
    end
end

local function fetchSkillMeta(char, ref)
    if not ref then return nil end
    local bucket = metaBucket(char)
    local idxNum = tonumber(ref)
    if idxNum and bucket.byIndex[idxNum] then
        return bucket.byIndex[idxNum]
    end
    if bucket.byIndex[ref] then
        return bucket.byIndex[ref]
    end
    local refName = trim(ref)
    if refName ~= "" and bucket.byName[refName] then
        return bucket.byName[refName]
    end
    return nil
end

local function inheritSubFromMeta(char, sub)
    if type(sub) ~= "table" then return sub end
    local ref = sub.refM or sub.M or sub.m or sub.name or sub.n
    if not ref or trim(ref) == "" then
        return sub
    end
    local meta = fetchSkillMeta(char, ref)
    if not meta then
        return sub
    end

    local function pick(current, fallback)
        if current == false or current == 0 or current == "0" then
            return current
        end
        if current ~= nil and trim(tostring(current)) ~= "" then
            return current
        end
        return fallback
    end

    local hydrated = {}
    for k, v in pairs(sub) do
        hydrated[k] = v
    end
    hydrated.name = pick(sub.name or sub.n, meta.name)
    hydrated.icon = pick(sub.icon, meta.icon)
    hydrated.level = pick(sub.level, meta.level)
    hydrated.video = pick(sub.video, meta.video)
    hydrated.energy = pick(sub.energy, meta.energy)
    hydrated.powerpve = pick(sub.powerpve, meta.powerpve)
    hydrated.powerpvp = pick(sub.powerpvp, meta.powerpvp)
    hydrated.cooldown = pick(sub.cooldown, meta.cooldown)

    hydrated.desc = pick(sub.desc, meta.desc)
    hydrated.descPt = pick(sub.descPt, meta.descPt)
    hydrated.descEn = pick(sub.descEn, meta.descEn)
    hydrated.descEs = pick(sub.descEs, meta.descEs)
    hydrated.descPl = pick(sub.descPl, meta.descPl)

    -- PRESERVA weapon explicitamente (não herda de meta)
    if sub.weapon ~= nil then
        hydrated.weapon = sub.weapon
    end
    if sub.weaponPacked ~= nil then
        hydrated.weaponPacked = sub.weaponPacked
    end

    if type(sub.desc_i18n) == "table" then
        hydrated.desc_i18n = sub.desc_i18n
    else
        hydrated.desc_i18n = {
            pt = hydrated.descPt,
            en = hydrated.descEn,
            es = hydrated.descEs,
            pl = hydrated.descPl
        }
    end

    if type(sub.subs) == "table" then
        hydrated.subs = {}
        for i, nested in ipairs(sub.subs) do
            hydrated.subs[i] = inheritSubFromMeta(char, nested)
        end
    end

    return hydrated
end

-- ===== Herança automática de descrições =====

local function findDescInSkills(data, skillName)
    if not data or not data.skills then return nil end
    local sk = data.skills[skillName]
    if sk and type(sk.desc) == "table" then
        return sk.desc
    end
    return nil
end

local function findDescInAnySubskills(data, skillName)
    if not data or not data.skills then return nil end
    for _, sk in pairs(data.skills) do
        if type(sk) == "table" and type(sk.subskills) == "table" then
            local sub = sk.subskills[skillName]
            if sub and type(sub.desc) == "table" then
                return sub.desc
            end
        end
    end
    return nil
end

local function resolveDescWithInheritance(data, skillName, directDesc)
    if directDesc and type(directDesc) == "table" and next(directDesc) then
        return directDesc
    end
    local fromSkills = findDescInSkills(data, skillName)
    if fromSkills then
        return fromSkills
    end
    local fromSubskills = findDescInAnySubskills(data, skillName)
    if fromSubskills then
        return fromSubskills
    end
    return nil
end

-- ===== Weapon =====
-- Retorna formato: icon~pve~pvp~cd~video~energy (separado por ~)
-- Isso evita problemas com JSON dentro de templates aninhados

function p.weapon(frame)
    local a = safeArgs(frame)
    local icon = trim(a.icon or "") ~= "" and a.icon or "Nada.png"
    local pve = trim(a.powerpve or "")
    local pvp = trim(a.powerpvp or "")
    local cd = trim(a.cooldown or "")
    local video = trim(a.video or "")
    local energy = trim(a.energy or "")
    -- Formato: icon~pve~pvp~cd~video~energy
    return icon .. "~" .. pve .. "~" .. pvp .. "~" .. cd .. "~" .. video .. "~" .. energy
end

-- Helper para parsear formato de weapon (icon~pve~pvp~cd~video~energy)
local function parseWeaponString(str)
    if not str or trim(str) == "" then return nil end
    str = trim(str)

    -- Se começa com {, é JSON (formato antigo)
    if str:sub(1, 1) == "{" then
        local ok, data = pcall(mw.text.jsonDecode, str)
        if ok and type(data) == "table" then
            local result = {
                icon = data.icon or "Nada.png"
            }
            -- Só inclui campos que existem (não inclui nil)
            if data.powerpve ~= nil then result.powerpve = data.powerpve end
            if data.powerpvp ~= nil then result.powerpvp = data.powerpvp end
            if data.cooldown ~= nil then result.cooldown = data.cooldown end
            if data.energy ~= nil then result.energy = data.energy end
            if data.video and trim(data.video) ~= "" then result.video = data.video end
            return result
        end
        return nil
    end

    -- Formato novo: icon~pve~pvp~cd~video~energy
    -- IMPORTANTE: precisa preservar campos vazios para manter ordem fixa
    local parts = {}
    -- Split manual que preserva campos vazios
    local start = 1
    while true do
        local pos = str:find("~", start)
        if not pos then
            -- Último campo
            table.insert(parts, trim(str:sub(start)))
            break
        end
        table.insert(parts, trim(str:sub(start, pos - 1)))
        start = pos + 1
    end
    -- Garante que temos pelo menos 6 campos (preenche com "" se necessário)
    while #parts < 6 do
        table.insert(parts, "")
    end
    -- Também aceita formato com vírgula: pve,pvp,cd,video
    if #parts < 2 then
        parts = {}
        for part in str:gmatch("[^,]+") do
            table.insert(parts, trim(part))
        end
        if #parts >= 1 then
            local result = {
                icon = "Nada.png"
            }
            -- Só inclui campos que existem (não inclui nil)
            if nz(parts[1]) then result.powerpve = parts[1] end
            if nz(parts[2]) then result.powerpvp = parts[2] end
            if nz(parts[3]) then result.cooldown = parts[3] end
            if parts[4] and trim(parts[4]) ~= "" then result.video = parts[4] end
            if nz(parts[5]) then result.energy = parts[5] end
            return result
        end
        return nil
    end

    -- Garante que temos pelo menos as partes básicas (icon sempre existe)
    local result = {
        icon = nz(parts[1]) or "Nada.png"
    }
    -- Só inclui campos que existem (não inclui nil)
    if nz(parts[2]) then result.powerpve = parts[2] end
    if nz(parts[3]) then result.powerpvp = parts[3] end
    if nz(parts[4]) then result.cooldown = parts[4] end
    if parts[5] and trim(parts[5]) ~= "" then result.video = parts[5] end
    if nz(parts[6]) then result.energy = parts[6] end
    return result
end

local function assignWeaponFromArg(obj, weaponStr)
    if not obj then return end
    local packed = trim(weaponStr or "")
    if packed ~= "" then
        -- Remove quebras de linha e espaços extras que podem vir da expansão do MediaWiki
        packed = packed:gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "")
        obj.weaponPacked = packed
        local parsed = parseWeaponString(packed)
        if parsed then
            obj.weapon = parsed
        end
    end
end

-- Normaliza uma tabela weapon vinda do módulo do personagem
-- Aceita tanto weapon.desc quanto weapon.desc_i18n e sempre retorna weapon.desc_i18n
local function normalizeWeaponTable(raw)
    if type(raw) ~= "table" then
        return nil
    end

    local w = {}
    -- Só inclui campos que existem (não inclui nil)
    if raw.icon then w.icon = raw.icon else w.icon = "Nada.png" end
    if raw.powerpve ~= nil then w.powerpve = raw.powerpve end
    if raw.powerpvp ~= nil then w.powerpvp = raw.powerpvp end
    if raw.cooldown ~= nil then w.cooldown = raw.cooldown end
    if raw.energy ~= nil then w.energy = raw.energy end
    -- Só inclui video se não estiver vazio
    if raw.video and trim(raw.video) ~= "" then
        w.video = raw.video
    end

    -- Aceita tanto raw.desc_i18n quanto raw.desc
    local src = nil
    if type(raw.desc_i18n) == "table" then
        src = raw.desc_i18n
    elseif type(raw.desc) == "table" then
        src = raw.desc
    end

    if src then
        local desc_i18n = {}
        for _, code in ipairs({ "pt", "en", "es", "pl" }) do
            local d = src[code]
            if d and trim(d) ~= "" then
                -- Usa a função colorize já existente no módulo
                desc_i18n[code] = colorize(d)
            end
        end
        if next(desc_i18n) ~= nil then
            w.desc_i18n = desc_i18n
        end
    end

    return w
end

local function ensureWeaponHydrated(sub)
    if type(sub) ~= "table" then return end
    -- Se weapon já é uma tabela válida, normaliza para garantir desc_i18n
    if sub.weapon and type(sub.weapon) == "table" and next(sub.weapon) ~= nil then
        local normalized = normalizeWeaponTable(sub.weapon)
        if normalized then
            sub.weapon = normalized
        end
        return -- Já está hidratado e normalizado
    end
    -- Se weapon é string, tenta parsear
    if sub.weapon and type(sub.weapon) ~= "table" then
        local parsed = parseWeaponString(sub.weapon)
        if parsed and next(parsed) ~= nil then
            sub.weapon = parsed
        else
            -- Se parse falhou mas tem weaponPacked, tenta depois
            if not sub.weaponPacked then
                sub.weaponPacked = sub.weapon
            end
            sub.weapon = nil
        end
    end
    -- Se não tem weapon mas tem weaponPacked, tenta parsear
    if not sub.weapon and sub.weaponPacked and trim(sub.weaponPacked) ~= "" then
        local parsed = parseWeaponString(sub.weaponPacked)
        if parsed and next(parsed) ~= nil then
            sub.weapon = parsed
        end
    end
end

local function normalizeSubTree(sub)
    if type(sub) ~= "table" then return end
    -- Preserva weapon antes de hidratar
    local hadWeapon = sub.weapon ~= nil
    local weaponBefore = sub.weapon
    ensureWeaponHydrated(sub)
    -- Se tinha weapon mas perdeu após hidratação, tenta recuperar do weaponPacked
    if hadWeapon and not sub.weapon and sub.weaponPacked then
        local parsed = parseWeaponString(sub.weaponPacked)
        if parsed and next(parsed) ~= nil then
            sub.weapon = parsed
        end
    end
    -- Se tinha weapon como objeto válido antes, preserva mesmo se ensureWeaponHydrated removeu
    if weaponBefore and type(weaponBefore) == "table" and next(weaponBefore) ~= nil and not sub.weapon then
        sub.weapon = weaponBefore
    end
    if type(sub.subs) == "table" then
        for _, nested in ipairs(sub.subs) do
            normalizeSubTree(nested)
        end
    end
    -- Só remove weaponPacked se weapon está válido
    if sub.weapon and type(sub.weapon) == "table" and next(sub.weapon) ~= nil then
        sub.weaponPacked = nil
    end
end

-- ===== Skill (com M) =====

function p.skill(frame)
    local a = safeArgs(frame)
    local lang = trim((a.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower())
    local char = resolveCharFromFrames(frame, a)
    local data = requireCharacterModule(char) or {}

    local name, desc_i18n = nil, {}
    if a.M and trim(a.M) ~= "" then
        local m = tonumber(a.M) or 0
        local order = data.order or {}
        local key = order[m] or ""
        local sk = (data.skills or {})[key] or {}
        name = trim(sk.name or key or "")
        if type(sk.desc) == "table" then
            local langs = { "pt", "en", "es", "pl" }
            for _, code in ipairs(langs) do
                local d = sk.desc[code]
                if d and trim(d) ~= "" then
                    desc_i18n[code] = colorize(d)
                end
            end
        elseif sk.desc and trim(sk.desc) ~= "" then
            desc_i18n["pt"] = colorize(sk.desc)
        end
    end

    local chosen = desc_i18n[lang] or desc_i18n["pt"] or ""
    local flagsArr = parseFlags(a.flags)

    local obj = {
        icon = (trim(a.icon or "") ~= "" and a.icon or "Nada.png"),
        level = (trim(a.level or "") ~= "" and a.level or "NIVEL"),
        energy = nz(a.energy),
        powerpve = nz(a.powerpve),
        powerpvp = nz(a.powerpvp),
        cooldown = nz(a.cooldown),
        video = a.video or "",
        name = name
    }
    if #flagsArr > 0 then
        obj.flags = flagsArr
    end
    if chosen ~= "" then
        obj.desc = chosen
    end
    if next(desc_i18n) ~= nil then
        obj.desc_i18n = desc_i18n
        obj.descPt = desc_i18n.pt
        obj.descEn = desc_i18n.en
        obj.descEs = desc_i18n.es
        obj.descPl = desc_i18n.pl
    end

    -- Registra metadados desta skill para herança futura
    if a.M and trim(a.M) ~= "" then
        local metaData = {
            name = obj.name or name or "",
            icon = obj.icon,
            level = obj.level,
            energy = obj.energy,
            powerpve = obj.powerpve,
            powerpvp = obj.powerpvp,
            cooldown = obj.cooldown,
            video = obj.video,
            desc = obj.desc,
            descPt = obj.descPt,
            descEn = obj.descEn,
            descEs = obj.descEs,
            descPl = obj.descPl
        }
        storeSkillMeta(char, tonumber(a.M) or a.M, metaData.name, metaData)
    end

    -- Subskills vindas via |subs=
    do
        local subsPacked = a.subs or ""
        local subsArr = {}

        -- Primeiro: extrai índices numéricos do início (antes dos JSONs)
        -- Formato: "1,2,3,4,5 {json1}{json2}" ou só "1,2,3,4,5"
        local indexPart = subsPacked:match("^([%s%d,]+)")
        local addedIndices = {} -- Track which indices were added
        if indexPart then
            for idx in indexPart:gmatch("(%d+)") do
                local i = tonumber(idx)
                if i and data and data.order and data.order[i] then
                    local skillName = data.order[i]
                    local skillData = (data.skills or {})[skillName] or {}
                    local subObj = {
                        refS = tostring(i),
                        name = skillName,
                        n = skillName,
                        icon = "", -- JS vai herdar da skill principal
                        level = "",
                        video = "",
                        energy = nil,
                        powerpve = nil,
                        powerpvp = nil,
                        cooldown = nil
                    }
                    -- Busca descrição do módulo
                    if type(skillData.desc) == "table" then
                        local desc_i18n = {}
                        local langs = { "pt", "en", "es", "pl" }
                        for _, code in ipairs(langs) do
                            local d = skillData.desc[code]
                            if d and trim(d) ~= "" then
                                desc_i18n[code] = colorize(d)
                            end
                        end
                        if next(desc_i18n) then
                            subObj.desc_i18n = desc_i18n
                            subObj.descPt = desc_i18n.pt
                            subObj.descEn = desc_i18n.en
                            subObj.descEs = desc_i18n.es
                            subObj.descPl = desc_i18n.pl
                        end
                    end
                    table.insert(subsArr, subObj)
                    addedIndices[tostring(i)] = #subsArr -- Store position
                end
            end
        end

        -- Depois: processa JSONs (das {{Subskill}} expandidas)
        local chunks = collectJsonObjects(subsPacked)

        -- Se um JSON tem refM que já foi adicionado via índice, substitui/merge
        for _, chunk in ipairs(chunks) do
            local ok, sk = pcall(mw.text.jsonDecode, chunk)
            if ok and type(sk) == "table" then
                ensureWeaponHydrated(sk)
                local ref = sk.refM or sk.M or sk.m or sk.refS or sk.S or sk.s
                if ref and addedIndices[tostring(ref)] then
                    -- Merge: atualiza o objeto existente com os atributos do JSON
                    local pos = addedIndices[tostring(ref)]
                    local existing = subsArr[pos]
                    -- Sobrescreve apenas atributos não vazios do JSON
                    if sk.video and trim(sk.video) ~= "" then existing.video = sk.video end
                    if sk.icon and trim(sk.icon) ~= "" then existing.icon = sk.icon end
                    if sk.level and trim(tostring(sk.level)) ~= "" then existing.level = sk.level end
                    if sk.energy ~= nil then existing.energy = sk.energy end
                    if sk.powerpve ~= nil then existing.powerpve = sk.powerpve end
                    if sk.powerpvp ~= nil then existing.powerpvp = sk.powerpvp end
                    if sk.cooldown ~= nil then existing.cooldown = sk.cooldown end
                    if sk.weapon ~= nil then existing.weapon = sk.weapon end
                    if sk.weaponPacked ~= nil then existing.weaponPacked = sk.weaponPacked end

                    -- BUSCA WEAPON DO MÓDULO LUA para fazer merge
                    -- Se weapon já existe mas não tem desc_i18n, busca do módulo para completar
                    local subName = existing.name or existing.n or ""
                    if subName ~= "" then
                        for _, skillKey in ipairs(data.order or {}) do
                            local skillData = (data.skills or {})[skillKey]
                            if type(skillData) == "table" and type(skillData.subskills) == "table" then
                                local moduleSub = skillData.subskills[subName]
                                if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then
                                    local moduleWeapon = normalizeWeaponTable(moduleSub.weapon)
                                    if moduleWeapon then
                                        if existing.weapon and type(existing.weapon) == "table" then
                                            -- Merge: preserva atributos do template, adiciona desc_i18n do módulo
                                            if moduleWeapon.desc_i18n and (not existing.weapon.desc_i18n) then
                                                existing.weapon.desc_i18n = moduleWeapon.desc_i18n
                                            end
                                            -- Se não tem icon no template, usa do módulo
                                            if (not existing.weapon.icon or existing.weapon.icon == "Nada.png") and moduleWeapon.icon then
                                                existing.weapon.icon = moduleWeapon.icon
                                            end
                                            -- Se não tem atributos no template, usa do módulo
                                            if existing.weapon.powerpve == nil and moduleWeapon.powerpve ~= nil then
                                                existing.weapon.powerpve = moduleWeapon.powerpve
                                            end
                                            if existing.weapon.powerpvp == nil and moduleWeapon.powerpvp ~= nil then
                                                existing.weapon.powerpvp = moduleWeapon.powerpvp
                                            end
                                            if existing.weapon.cooldown == nil and moduleWeapon.cooldown ~= nil then
                                                existing.weapon.cooldown = moduleWeapon.cooldown
                                            end
                                            if existing.weapon.energy == nil and moduleWeapon.energy ~= nil then
                                                existing.weapon.energy = moduleWeapon.energy
                                            end
                                            if (not existing.weapon.video or existing.weapon.video == "") and moduleWeapon.video then
                                                existing.weapon.video = moduleWeapon.video
                                            end
                                        else
                                            -- Se não tem weapon ainda, usa o do módulo
                                            existing.weapon = moduleWeapon
                                        end
                                        break
                                    end
                                end
                            end
                        end
                    end

                    -- Garante que weapon está hidratado após merge
                    ensureWeaponHydrated(existing)
                    -- Marca como já processado para não adicionar de novo
                    sk._merged = true
                end
            end
        end
        for _, chunk in ipairs(chunks) do
            local ok, sk = pcall(mw.text.jsonDecode, chunk)
            if ok and type(sk) == "table" then
                ensureWeaponHydrated(sk)
                -- Pula se já foi mergeado com um índice
                local ref = sk.refM or sk.M or sk.m or sk.refS or sk.S or sk.s
                local skipChunk = false
                if ref and addedIndices[tostring(ref)] then
                    -- Já foi processado no merge acima, pula
                    skipChunk = true
                end

                if not skipChunk then
                    -- Se tem ref (M ou S), aplica herança do cache OU busca diretamente
                    if ref and trim(tostring(ref)) ~= "" then
                        local mainMeta = fetchSkillMeta(char, ref)

                        -- Se não tem no cache, ref corresponde a M=N, então busca pelo índice
                        if not mainMeta then
                            local idx = tonumber(ref)
                            if idx and data and data.order and data.order[idx] then
                                local skillName = data.order[idx]
                                local skillData = (data.skills or {})[skillName] or {}
                                -- Cria um meta básico com dados do módulo
                                mainMeta = {
                                    name = skillName,
                                    icon = "", -- será preenchido pelo JS
                                    level = "",
                                    video = "",
                                    energy = nil,
                                    powerpve = nil,
                                    powerpvp = nil,
                                    cooldown = nil
                                }
                                -- Busca descrição
                                if type(skillData.desc) == "table" then
                                    local langs = { "pt", "en", "es", "pl" }
                                    for _, code in ipairs(langs) do
                                        local d = skillData.desc[code]
                                        if d and trim(d) ~= "" then
                                            mainMeta["desc" .. code:sub(1, 1):upper() .. code:sub(2)] = colorize(d)
                                        end
                                    end
                                end
                            end
                        end

                        if mainMeta then
                            -- Preenche nome se não tiver
                            if (not sk.name or trim(sk.name) == "") and (not sk.n or trim(sk.n) == "") then
                                sk.name = mainMeta.name
                                sk.n = mainMeta.name
                            end
                            -- Herda atributos vazios
                            if not sk.icon or trim(sk.icon) == "" then sk.icon = mainMeta.icon end
                            if not sk.level or trim(tostring(sk.level)) == "" then sk.level = mainMeta.level end
                            if not sk.video or trim(sk.video) == "" then sk.video = mainMeta.video end
                            if sk.energy == nil then sk.energy = mainMeta.energy end
                            if sk.powerpve == nil then sk.powerpve = mainMeta.powerpve end
                            if sk.powerpvp == nil then sk.powerpvp = mainMeta.powerpvp end
                            if sk.cooldown == nil then sk.cooldown = mainMeta.cooldown end
                            -- Herda descrição
                            if not sk.desc_i18n and not sk.descPt then
                                -- Busca descrição do módulo pelo nome
                                local foundDesc = resolveDescWithInheritance(data, mainMeta.name, nil)
                                if foundDesc then
                                    local langs = { "pt", "en", "es", "pl" }
                                    for _, code in ipairs(langs) do
                                        local d = foundDesc[code]
                                        if d and trim(d) ~= "" then
                                            sk["desc" .. code:sub(1, 1):upper() .. code:sub(2)] = colorize(d)
                                        end
                                    end
                                    sk.desc_i18n = {
                                        pt = sk.descPt,
                                        en = sk.descEn,
                                        es = sk.descEs,
                                        pl = sk.descPl
                                    }
                                elseif mainMeta.descPt or mainMeta.descEn then
                                    sk.descPt = mainMeta.descPt
                                    sk.descEn = mainMeta.descEn
                                    sk.descEs = mainMeta.descEs
                                    sk.descPl = mainMeta.descPl
                                    sk.desc_i18n = {
                                        pt = mainMeta.descPt,
                                        en = mainMeta.descEn,
                                        es = mainMeta.descEs,
                                        pl = mainMeta.descPl
                                    }
                                end
                            end
                        end

                        -- Mesmo sem mainMeta, mantém refM para o JS processar
                        if not mainMeta then
                            sk.refM = ref
                        end
                    end
                    -- Fallback: Se tem refM explícito no JSON, aplica herança do cache
                    local refMExplicit = sk.refM or sk.M or sk.m
                    if refMExplicit and trim(tostring(refMExplicit)) ~= "" and not ref then
                        local mainMeta = fetchSkillMeta(char, refMExplicit)
                        if mainMeta then
                            -- Preenche nome se não tiver
                            if (not sk.name or trim(sk.name) == "") and (not sk.n or trim(sk.n) == "") then
                                sk.name = mainMeta.name
                                sk.n = mainMeta.name
                            end
                            -- Herda atributos vazios
                            if not sk.icon or trim(sk.icon) == "" then sk.icon = mainMeta.icon end
                            if not sk.level or trim(tostring(sk.level)) == "" then sk.level = mainMeta.level end
                            if not sk.video or trim(sk.video) == "" then sk.video = mainMeta.video end
                            if sk.energy == nil then sk.energy = mainMeta.energy end
                            if sk.powerpve == nil then sk.powerpve = mainMeta.powerpve end
                            if sk.powerpvp == nil then sk.powerpvp = mainMeta.powerpvp end
                            if sk.cooldown == nil then sk.cooldown = mainMeta.cooldown end
                            -- Herda descrição
                            if not sk.desc_i18n and not sk.descPt then
                                sk.descPt = mainMeta.descPt
                                sk.descEn = mainMeta.descEn
                                sk.descEs = mainMeta.descEs
                                sk.descPl = mainMeta.descPl
                                sk.desc_i18n = {
                                    pt = mainMeta.descPt,
                                    en = mainMeta.descEn,
                                    es = mainMeta.descEs,
                                    pl = mainMeta.descPl
                                }
                            end
                        end
                    end

                    -- BUSCA WEAPON DO MÓDULO LUA para fazer merge
                    -- Se weapon já existe mas não tem desc_i18n, busca do módulo para completar
                    local subName = sk.name or sk.n or ""
                    if subName ~= "" then
                        for _, skillKey in ipairs(data.order or {}) do
                            local skillData = (data.skills or {})[skillKey]
                            if type(skillData) == "table" and type(skillData.subskills) == "table" then
                                local moduleSub = skillData.subskills[subName]
                                if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then
                                    local moduleWeapon = normalizeWeaponTable(moduleSub.weapon)
                                    if moduleWeapon then
                                        if sk.weapon and type(sk.weapon) == "table" then
                                            -- Merge: preserva atributos do template, adiciona desc_i18n do módulo
                                            if moduleWeapon.desc_i18n and (not sk.weapon.desc_i18n) then
                                                sk.weapon.desc_i18n = moduleWeapon.desc_i18n
                                            end
                                            -- Se não tem icon no template, usa do módulo
                                            if (not sk.weapon.icon or sk.weapon.icon == "Nada.png") and moduleWeapon.icon then
                                                sk.weapon.icon = moduleWeapon.icon
                                            end
                                            -- Se não tem atributos no template, usa do módulo
                                            if sk.weapon.powerpve == nil and moduleWeapon.powerpve ~= nil then
                                                sk.weapon.powerpve = moduleWeapon.powerpve
                                            end
                                            if sk.weapon.powerpvp == nil and moduleWeapon.powerpvp ~= nil then
                                                sk.weapon.powerpvp = moduleWeapon.powerpvp
                                            end
                                            if sk.weapon.cooldown == nil and moduleWeapon.cooldown ~= nil then
                                                sk.weapon.cooldown = moduleWeapon.cooldown
                                            end
                                            if sk.weapon.energy == nil and moduleWeapon.energy ~= nil then
                                                sk.weapon.energy = moduleWeapon.energy
                                            end
                                            if (not sk.weapon.video or sk.weapon.video == "") and moduleWeapon.video then
                                                sk.weapon.video = moduleWeapon.video
                                            end
                                        else
                                            -- Se não tem weapon ainda, usa o do módulo
                                            sk.weapon = moduleWeapon
                                        end
                                        break
                                    end
                                end
                            end
                        end
                    end

                    -- Garante que weapon está normalizado antes de adicionar
                    ensureWeaponHydrated(sk)
                    table.insert(subsArr, sk)
                end
            end -- end if not skipChunk
        end
        -- Fallback: subskills do módulo .lua
        if #subsArr == 0 and a.M and tonumber(a.M) then
            local m = tonumber(a.M)
            local order = (data and data.order) or {}
            local key = order[m] or ""
            local sk = (data and data.skills and data.skills[key]) or {}
            local suborder = type(sk) == "table" and sk.suborder or nil
            local subskills = type(sk) == "table" and sk.subskills or nil
            if type(suborder) == "table" and type(subskills) == "table" then
                for _, n in ipairs(suborder) do
                    local sub = subskills[n]
                    if type(sub) == "table" then
                        local resolvedDesc = resolveDescWithInheritance(data, n, sub.desc)

                        -- Se tem inherit, busca atributos da skill principal referenciada
                        local inheritIdx = sub.inherit
                        local inheritedIcon, inheritedLevel, inheritedVideo = "", "", ""
                        local inheritedEnergy, inheritedPvE, inheritedPvP, inheritedCD = nil, nil, nil, nil

                        if inheritIdx and tonumber(inheritIdx) then
                            local mainSkillMeta = fetchSkillMeta(char, inheritIdx)
                            if mainSkillMeta then
                                inheritedIcon = mainSkillMeta.icon or ""
                                inheritedLevel = mainSkillMeta.level or ""
                                inheritedVideo = mainSkillMeta.video or ""
                                inheritedEnergy = mainSkillMeta.energy
                                inheritedPvE = mainSkillMeta.powerpve
                                inheritedPvP = mainSkillMeta.powerpvp
                                inheritedCD = mainSkillMeta.cooldown
                                -- Se não tem descrição própria, herda da skill principal
                                if not resolvedDesc then
                                    resolvedDesc = {
                                        pt = mainSkillMeta.descPt,
                                        en = mainSkillMeta.descEn,
                                        es = mainSkillMeta.descEs,
                                        pl = mainSkillMeta.descPl
                                    }
                                end
                            end
                        end

                        -- Coloriza as descrições
                        local desc_i18n = {}
                        if resolvedDesc then
                            local langs = { "pt", "en", "es", "pl" }
                            for _, code in ipairs(langs) do
                                local d = resolvedDesc[code]
                                if d and trim(d) ~= "" then
                                    desc_i18n[code] = colorize(d)
                                end
                            end
                        end

                        local subObj = {
                            n = n,
                            name = n,
                            icon = (sub.icon and sub.icon ~= "") and sub.icon or inheritedIcon,
                            level = (sub.level and sub.level ~= "") and tostring(sub.level) or inheritedLevel,
                            video = (sub.video and sub.video ~= "") and sub.video or inheritedVideo,
                            energy = sub.energy or inheritedEnergy,
                            powerpve = sub.powerpve or inheritedPvE,
                            powerpvp = sub.powerpvp or inheritedPvP,
                            cooldown = sub.cooldown or inheritedCD,
                            desc_i18n = next(desc_i18n) and desc_i18n or nil,
                            descPt = desc_i18n.pt or nil,
                            descEn = desc_i18n.en or nil,
                            descEs = desc_i18n.es or nil,
                            descPl = desc_i18n.pl or nil
                        }
                        -- INCLUI WEAPON do módulo Lua se existir
                        if type(sub.weapon) == "table" then
                            local weaponObj = normalizeWeaponTable(sub.weapon)
                            if weaponObj then
                                subObj.weapon = weaponObj
                            end
                        end
                        if type(sub.suborder) == "table" and type(sub.subskills) == "table" then
                            local nested = {}
                            for _, nn in ipairs(sub.suborder) do
                                local s2 = sub.subskills[nn]
                                if type(s2) == "table" then
                                    local nestedDesc = resolveDescWithInheritance(data, nn, s2.desc)
                                    local nestedObj = {
                                        n = nn,
                                        name = nn,
                                        icon = s2.icon or "",
                                        level = s2.level or "",
                                        video = s2.video or "",
                                        energy = s2.energy,
                                        powerpve = s2.powerpve,
                                        powerpvp = s2.powerpvp,
                                        cooldown = s2.cooldown,
                                        desc_i18n = nestedDesc,
                                        descPt = nestedDesc and nestedDesc.pt or nil,
                                        descEn = nestedDesc and nestedDesc.en or nil,
                                        descEs = nestedDesc and nestedDesc.es or nil,
                                        descPl = nestedDesc and nestedDesc.pl or nil
                                    }
                                    -- INCLUI WEAPON do nested se existir
                                    if type(s2.weapon) == "table" then
                                        local weaponObj = normalizeWeaponTable(s2.weapon)
                                        if weaponObj then
                                            nestedObj.weapon = weaponObj
                                        end
                                    end
                                    table.insert(nested, nestedObj)
                                end
                            end
                            if #nested > 0 then subObj.subs = nested end
                        end
                        table.insert(subsArr, subObj)
                    end
                end
            end
        end
        if #subsArr > 0 then
            for i, sub in ipairs(subsArr) do
                subsArr[i] = inheritSubFromMeta(char, sub)
                -- BUSCA WEAPON DO MÓDULO LUA se não tem weapon ainda (após herança)
                local hasWeapon = subsArr[i].weapon and (
                    (type(subsArr[i].weapon) == "table" and next(subsArr[i].weapon) ~= nil) or
                    (type(subsArr[i].weapon) == "string" and trim(subsArr[i].weapon) ~= "")
                )
                if not hasWeapon then
                    local subName = subsArr[i].name or subsArr[i].n or ""
                    if subName ~= "" then
                        for _, skillKey in ipairs(data.order or {}) do
                            local skillData = (data.skills or {})[skillKey]
                            if type(skillData) == "table" and type(skillData.subskills) == "table" then
                                local moduleSub = skillData.subskills[subName]
                                if moduleSub then
                                    -- DEBUG: Log temporário
                                    if subName == "Karmic Jishin" then
                                        -- Verifica se tem weapon
                                    end
                                end
                                if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then
                                    local weaponObj = {
                                        icon = moduleSub.weapon.icon or "Nada.png",
                                        energy = moduleSub.weapon.energy,
                                        powerpve = moduleSub.weapon.powerpve,
                                        powerpvp = moduleSub.weapon.powerpvp,
                                        cooldown = moduleSub.weapon.cooldown,
                                        video = moduleSub.weapon.video or ""
                                    }
                                    if type(moduleSub.weapon.desc) == "table" then
                                        local weaponDesc = {}
                                        for _, code in ipairs({ "pt", "en", "es", "pl" }) do
                                            local d = moduleSub.weapon.desc[code]
                                            if d and trim(d) ~= "" then
                                                weaponDesc[code] = colorize(d)
                                            end
                                        end
                                        if next(weaponDesc) then
                                            weaponObj.desc_i18n = weaponDesc
                                        end
                                    end
                                    subsArr[i].weapon = weaponObj
                                    -- DEBUG: Log para confirmar que encontrou
                                    if subName == "Karmic Jishin" then
                                        -- Log temporário
                                    end
                                    break
                                end
                            end
                        end
                    end
                end
                normalizeSubTree(subsArr[i])
                -- VERIFICAÇÃO FINAL: Garante que weapon está presente e completo antes de serializar
                -- Busca do módulo para fazer merge (mesmo se já tem weapon do template)
                local subName = subsArr[i].name or subsArr[i].n or ""
                if subName ~= "" then
                    for _, skillKey in ipairs(data.order or {}) do
                        local skillData = (data.skills or {})[skillKey]
                        if type(skillData) == "table" and type(skillData.subskills) == "table" then
                            local moduleSub = skillData.subskills[subName]
                            if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then
                                local moduleWeapon = normalizeWeaponTable(moduleSub.weapon)
                                if moduleWeapon then
                                    if subsArr[i].weapon and type(subsArr[i].weapon) == "table" then
                                        -- Merge: preserva atributos do template, adiciona desc_i18n do módulo
                                        if moduleWeapon.desc_i18n and (not subsArr[i].weapon.desc_i18n) then
                                            subsArr[i].weapon.desc_i18n = moduleWeapon.desc_i18n
                                        end
                                        -- Se não tem icon no template, usa do módulo
                                        if (not subsArr[i].weapon.icon or subsArr[i].weapon.icon == "Nada.png") and moduleWeapon.icon then
                                            subsArr[i].weapon.icon = moduleWeapon.icon
                                        end
                                        -- Se não tem atributos no template, usa do módulo
                                        if subsArr[i].weapon.powerpve == nil and moduleWeapon.powerpve ~= nil then
                                            subsArr[i].weapon.powerpve = moduleWeapon.powerpve
                                        end
                                        if subsArr[i].weapon.powerpvp == nil and moduleWeapon.powerpvp ~= nil then
                                            subsArr[i].weapon.powerpvp = moduleWeapon.powerpvp
                                        end
                                        if subsArr[i].weapon.cooldown == nil and moduleWeapon.cooldown ~= nil then
                                            subsArr[i].weapon.cooldown = moduleWeapon.cooldown
                                        end
                                        if subsArr[i].weapon.energy == nil and moduleWeapon.energy ~= nil then
                                            subsArr[i].weapon.energy = moduleWeapon.energy
                                        end
                                        if (not subsArr[i].weapon.video or subsArr[i].weapon.video == "") and moduleWeapon.video then
                                            subsArr[i].weapon.video = moduleWeapon.video
                                        end
                                    else
                                        -- Se não tem weapon ainda, usa o do módulo
                                        subsArr[i].weapon = moduleWeapon
                                    end
                                    break
                                end
                            end
                        end
                    end
                end
            end
            obj.subs = subsArr
            -- SISTEMA UNIFICADO DE WEAPON PARA SUBSKILLS: Garante weapon normalizado e completo antes de serializar
            if type(obj.subs) == "table" then
                for i, sub in ipairs(obj.subs) do
                    local subName = sub.name or sub.n or ""

                    -- PASSO 1: Normaliza weapon existente
                    ensureWeaponHydrated(sub)
                    normalizeSubTree(sub)

                    -- PASSO 2: Se weapon ainda é string, tenta parsear
                    if sub.weapon and type(sub.weapon) ~= "table" then
                        local parsed = parseWeaponString(sub.weapon)
                        if parsed and next(parsed) ~= nil then
                            sub.weapon = parsed
                        else
                            sub.weapon = nil
                        end
                    end

                    -- PASSO 3: Busca weapon do módulo (sempre, para garantir merge completo)
                    if subName ~= "" then
                        for _, skillKey in ipairs(data.order or {}) do
                            local skillData = (data.skills or {})[skillKey]
                            if type(skillData) == "table" and type(skillData.subskills) == "table" then
                                -- Tenta match exato primeiro
                                local moduleSub = skillData.subskills[subName]
                                -- Se não encontrou, tenta case-insensitive
                                if not moduleSub then
                                    for k, v in pairs(skillData.subskills) do
                                        if type(k) == "string" and k:lower() == subName:lower() then
                                            moduleSub = v
                                            break
                                        end
                                    end
                                end

                                -- Se encontrou subskill no módulo com weapon, faz merge completo
                                if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then
                                    local moduleWeapon = normalizeWeaponTable(moduleSub.weapon)
                                    if moduleWeapon then
                                        if obj.subs[i].weapon and type(obj.subs[i].weapon) == "table" then
                                            -- MERGE: Combina weapon do template com weapon do módulo
                                            -- Descrição do módulo sempre tem prioridade (tem desc_i18n completo)
                                            if moduleWeapon.desc_i18n then
                                                obj.subs[i].weapon.desc_i18n = moduleWeapon.desc_i18n
                                            end
                                            -- Atributos: usa do módulo se não tem no template
                                            if not obj.subs[i].weapon.icon or obj.subs[i].weapon.icon == "Nada.png" then
                                                obj.subs[i].weapon.icon = moduleWeapon.icon
                                            end
                                            if obj.subs[i].weapon.powerpve == nil then
                                                obj.subs[i].weapon.powerpve = moduleWeapon.powerpve
                                            end
                                            if obj.subs[i].weapon.powerpvp == nil then
                                                obj.subs[i].weapon.powerpvp = moduleWeapon.powerpvp
                                            end
                                            if obj.subs[i].weapon.cooldown == nil then
                                                obj.subs[i].weapon.cooldown = moduleWeapon.cooldown
                                            end
                                            if obj.subs[i].weapon.energy == nil then
                                                obj.subs[i].weapon.energy = moduleWeapon.energy
                                            end
                                            if not obj.subs[i].weapon.video or obj.subs[i].weapon.video == "" then
                                                obj.subs[i].weapon.video = moduleWeapon.video
                                            end
                                        else
                                            -- Se não tem weapon ainda, usa o do módulo completo
                                            obj.subs[i].weapon = moduleWeapon
                                        end
                                        break
                                    end
                                end
                            end
                        end
                    end

                    -- PASSO 4: Garante que weapon está normalizado e válido
                    if obj.subs[i].weapon then
                        ensureWeaponHydrated(obj.subs[i])
                        -- Remove weaponPacked se weapon está válido
                        if type(obj.subs[i].weapon) == "table" and next(obj.subs[i].weapon) ~= nil then
                            obj.subs[i].weaponPacked = nil
                            -- Garante que desc_i18n existe se tem descrição
                            if obj.subs[i].weapon.desc_i18n and type(obj.subs[i].weapon.desc_i18n) == "table" then
                                -- Já está normalizado
                            elseif obj.subs[i].weapon.desc and type(obj.subs[i].weapon.desc) == "table" then
                                -- Converte desc para desc_i18n
                                local desc_i18n = {}
                                for _, code in ipairs({ "pt", "en", "es", "pl" }) do
                                    local d = obj.subs[i].weapon.desc[code]
                                    if d and trim(d) ~= "" then
                                        desc_i18n[code] = colorize(d)
                                    end
                                end
                                if next(desc_i18n) ~= nil then
                                    obj.subs[i].weapon.desc_i18n = desc_i18n
                                end
                            end
                        else
                            obj.subs[i].weapon = nil
                        end
                    end
                end
            end
        end

        -- suborder do módulo
        if a.M and tonumber(a.M) then
            local m = tonumber(a.M)
            local order = (data and data.order) or {}
            local key = order[m] or ""
            local sk = (data and data.skills and data.skills[key]) or {}
            if type(sk) == "table" and type(sk.suborder) == "table" then
                obj.suborder = sk.suborder
            end
        end
    end

    -- Weapon
    do
        assignWeaponFromArg(obj, a.weapon)
        if obj.weapon then
            if a.M and tonumber(a.M) then
                local m = tonumber(a.M)
                local order = (data and data.order) or {}
                local key = order[m] or ""
                local sk = (data and data.skills and data.skills[key]) or {}
                if type(sk) == "table" and type(sk.weapon) == "table" then
                    local normalized = normalizeWeaponTable(sk.weapon)
                    if normalized then
                        -- Preserva campos já existentes em obj.weapon se houver
                        if obj.weapon then
                            -- Merge inteligente: só sobrescreve se o campo do módulo não estiver vazio
                            -- E só sobrescreve se o campo do template estiver vazio ou não existir
                            for k, v in pairs(normalized) do
                                -- Para video: SEMPRE preserva o do template se existir (não faz merge)
                                if k == "video" then
                                    -- Se o template tem vídeo, preserva (não sobrescreve com módulo)
                                    if obj.weapon.video and trim(obj.weapon.video) ~= "" then
                                        -- Mantém o vídeo do template, não faz nada
                                    elseif v and v ~= "" then
                                        -- Só usa do módulo se template não tem
                                        obj.weapon.video = v
                                    end
                                    -- Para icon: preserva o do template se não for Nada.png
                                elseif k == "icon" then
                                    if (not obj.weapon.icon or obj.weapon.icon == "" or obj.weapon.icon == "Nada.png") and v and v ~= "" and v ~= "Nada.png" then
                                        obj.weapon.icon = v
                                    end
                                    -- Para outros campos: só faz merge se o campo NÃO EXISTE no template (nil)
                                    -- Se o campo existe no template (mesmo que vazio), preserva o do template
                                else
                                    -- IMPORTANTE: só faz merge se o campo é nil (não existe), não se está vazio
                                    if obj.weapon[k] == nil and v ~= nil and v ~= "" then
                                        obj.weapon[k] = v
                                    end
                                end
                            end
                            -- Sempre adiciona desc_i18n do módulo se não tiver no template
                            if normalized.desc_i18n and not obj.weapon.desc_i18n then
                                obj.weapon.desc_i18n = normalized.desc_i18n
                            end
                        else
                            obj.weapon = normalized
                        end
                        -- Adiciona campos descPt, descEn, etc. para compatibilidade
                        if obj.weapon.desc_i18n then
                            obj.weapon.descPt = obj.weapon.desc_i18n.pt
                            obj.weapon.descEn = obj.weapon.desc_i18n.en
                            obj.weapon.descEs = obj.weapon.desc_i18n.es
                            obj.weapon.descPl = obj.weapon.desc_i18n.pl
                        end
                    end
                end
            end
        end
    end

    return mw.text.jsonEncode(obj)
end

-- ===== Emote =====

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

-- ===== Subskill =====
-- Gera JSON simples. Herança COMPLETA de atributos é feita no JavaScript.
-- Se |M= for passado sem |n=, retorna apenas refM para o JS resolver.

function p.subskill(frame)
    local a = safeArgs(frame)
    local lang = trim((a.lang or (frame:getParent() and frame:getParent().args.lang) or "pt"):lower())

    local n = trim(a.n or "")
    local refM = trim(a.m or a.M or "")
    local refS = trim(a.S or a.s or "")

    -- Prioriza |M= sobre |S= para herança
    local ref = (refM ~= "" and refM) or refS

    local char = resolveCharFromFrames(frame, a)
    local data = requireCharacterModule(char) or {}

    -- Se tem |M= ou |S=, busca do cache e herda atributos
    if ref ~= "" then
        local meta = fetchSkillMeta(char, ref)
        if meta then
            -- Herda atributos da skill referenciada, sobrescreve só o que foi passado
            local function pick(explicit, inherited)
                if explicit ~= nil and trim(tostring(explicit)) ~= "" then
                    return explicit
                end
                return inherited
            end

            -- Busca descrição: primeiro do módulo pelo nome herdado, depois usa a da skill
            local inheritedName = meta.name or ""
            local desc_i18n = {}
            local foundDesc = resolveDescWithInheritance(data, inheritedName, nil)
            if foundDesc then
                local langs = { "pt", "en", "es", "pl" }
                for _, code in ipairs(langs) do
                    local d = foundDesc[code]
                    if d and trim(d) ~= "" then
                        desc_i18n[code] = colorize(d)
                    end
                end
            end
            -- Se não achou no módulo, usa descrição da skill original
            if not next(desc_i18n) then
                desc_i18n = {
                    pt = meta.descPt,
                    en = meta.descEn,
                    es = meta.descEs,
                    pl = meta.descPl
                }
            end

            local obj = {
                name = pick(a.n, inheritedName),
                icon = pick(a.icon, meta.icon),
                level = pick(a.level, meta.level),
                video = pick(a.video, meta.video),
                energy = nz(a.energy) or meta.energy,
                powerpve = nz(a.powerpve) or meta.powerpve,
                powerpvp = nz(a.powerpvp) or meta.powerpvp,
                cooldown = nz(a.cooldown) or meta.cooldown,
                back = (trim(a.back or "") == "yes" or trim(a.back or "") == "true") and true or nil,
                refM = ref
            }

            local flagsArr = parseFlags(a.flags)
            if #flagsArr > 0 then
                obj.flags = flagsArr
            end
            if next(desc_i18n) then
                obj.desc_i18n = desc_i18n
                obj.descPt = desc_i18n.pt
                obj.descEn = desc_i18n.en
                obj.descEs = desc_i18n.es
                obj.descPl = desc_i18n.pl
            end

            -- Weapon para subskill
            assignWeaponFromArg(obj, a.weapon)

            return mw.text.jsonEncode(obj)
        else
            -- Cache vazio - busca nome e descrição diretamente do módulo
            -- ref corresponde ao índice M
            local idx = tonumber(ref)
            local skillName = ""
            local desc_i18n = {}

            if idx and data and data.order and data.order[idx] then
                skillName = data.order[idx]
                local skillData = (data.skills or {})[skillName] or {}
                if type(skillData.desc) == "table" then
                    local langs = { "pt", "en", "es", "pl" }
                    for _, code in ipairs(langs) do
                        local d = skillData.desc[code]
                        if d and trim(d) ~= "" then
                            desc_i18n[code] = colorize(d)
                        end
                    end
                end
            end

            local obj = {
                refM = ref,
                name = skillName,
                n = skillName,
                icon = trim(a.icon or ""),
                level = trim(a.level or ""),
                video = trim(a.video or ""),
                energy = nz(a.energy),
                powerpve = nz(a.powerpve),
                powerpvp = nz(a.powerpvp),
                cooldown = nz(a.cooldown),
                back = (trim(a.back or "") == "yes" or trim(a.back or "") == "true") and true or nil
            }
            if trim(a.n or "") ~= "" then
                obj.name = trim(a.n)
                obj.n = trim(a.n)
            end
            if next(desc_i18n) then
                obj.desc_i18n = desc_i18n
                obj.descPt = desc_i18n.pt
                obj.descEn = desc_i18n.en
                obj.descEs = desc_i18n.es
                obj.descPl = desc_i18n.pl
            end
            local flagsArr = parseFlags(a.flags)
            if #flagsArr > 0 then
                obj.flags = flagsArr
            end
            -- Weapon para subskill
            assignWeaponFromArg(obj, a.weapon)
            return mw.text.jsonEncode(obj)
        end
    end

    -- Se só tem refM (sem nome), retorna objeto mínimo para o JS fazer herança completa
    if n == "" and refM ~= "" then
        local obj = {
            refM = refM,
            icon = trim(a.icon or ""),
            level = trim(a.level or ""),
            video = trim(a.video or ""),
            energy = nz(a.energy),
            powerpve = nz(a.powerpve),
            powerpvp = nz(a.powerpvp),
            cooldown = nz(a.cooldown),
            back = (trim(a.back or "") == "yes" or trim(a.back or "") == "true") and true or nil
        }
        local flagsArr = parseFlags(a.flags)
        if #flagsArr > 0 then
            obj.flags = flagsArr
        end
        -- Weapon para subskill
        assignWeaponFromArg(obj, a.weapon)
        return mw.text.jsonEncode(obj)
    end

    -- Se não tem nome nem refM nem refS, retorna vazio
    if n == "" then
        return mw.text.jsonEncode({})
    end

    -- Busca descrição do módulo .lua
    local desc_i18n = {}
    local foundDesc = nil
    for _, key in ipairs(data.order or {}) do
        local sk = (data.skills or {})[key]
        if type(sk) == "table" and type(sk.subskills) == "table" then
            local sub = sk.subskills[n]
            if sub and type(sub.desc) == "table" then
                foundDesc = sub.desc
                break
            end
        end
    end
    if not foundDesc then
        foundDesc = resolveDescWithInheritance(data, n, nil)
    end
    if foundDesc then
        local langs = { "pt", "en", "es", "pl" }
        for _, code in ipairs(langs) do
            local d = foundDesc[code]
            if d and trim(d) ~= "" then
                desc_i18n[code] = colorize(d)
            end
        end
    end

    local flagsArr = parseFlags(a.flags)

    -- Objeto com nome explícito
    local obj = {
        name = n,
        icon = trim(a.icon or ""),
        level = trim(a.level or ""),
        video = trim(a.video or ""),
        energy = nz(a.energy),
        powerpve = nz(a.powerpve),
        powerpvp = nz(a.powerpvp),
        cooldown = nz(a.cooldown),
        back = (trim(a.back or "") == "yes" or trim(a.back or "") == "true") and true or nil
    }

    if refM ~= "" then
        obj.refM = refM
    end
    if #flagsArr > 0 then
        obj.flags = flagsArr
    end
    if next(desc_i18n) ~= nil then
        obj.desc_i18n = desc_i18n
        obj.descPt = desc_i18n.pt
        obj.descEn = desc_i18n.en
        obj.descEs = desc_i18n.es
        obj.descPl = desc_i18n.pl
    end

    -- Nested subs
    do
        local subsPacked = a.subs or ""
        local subsArr = {}
        for _, chunk in ipairs(collectJsonObjects(subsPacked)) do
            local ok, sub = pcall(mw.text.jsonDecode, chunk)
            if ok and type(sub) == "table" then
                ensureWeaponHydrated(sub)
                table.insert(subsArr, sub)
            end
        end
        if #subsArr > 0 then
            for _, sub in ipairs(subsArr) do
                -- BUSCA WEAPON DO MÓDULO LUA se não tem weapon ainda (para nested subs também)
                local hasWeapon = sub.weapon and (
                    (type(sub.weapon) == "table" and next(sub.weapon) ~= nil) or
                    (type(sub.weapon) == "string" and trim(sub.weapon) ~= "")
                )
                if not hasWeapon then
                    local subName = sub.name or sub.n or ""
                    if subName ~= "" then
                        for _, skillKey in ipairs(data.order or {}) do
                            local skillData = (data.skills or {})[skillKey]
                            if type(skillData) == "table" and type(skillData.subskills) == "table" then
                                local moduleSub = skillData.subskills[subName]
                                if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then
                                    local weaponObj = normalizeWeaponTable(moduleSub.weapon)
                                    if weaponObj then
                                        sub.weapon = weaponObj
                                        break
                                    end
                                end
                            end
                        end
                    end
                end
                normalizeSubTree(sub)
            end
            obj.subs = subsArr
        end
    end

    -- Weapon para subskill
    -- Usa {{Weapon|...}} que retorna formato: icon~pve~pvp~cd~video~energy
    do
        assignWeaponFromArg(obj, a.weapon)

        -- Busca descrição do weapon no módulo Lua (subskills)
        for _, key in ipairs(data.order or {}) do
            local sk = (data.skills or {})[key]
            if type(sk) == "table" and type(sk.subskills) == "table" then
                local sub = sk.subskills[n]
                if sub and type(sub.weapon) == "table" then
                    local normalized = normalizeWeaponTable(sub.weapon)
                    if normalized then
                        -- Preserva campos já existentes em obj.weapon se houver
                        if obj.weapon then
                            -- Merge inteligente: só sobrescreve se o campo do módulo não estiver vazio
                            -- E só sobrescreve se o campo do template estiver vazio ou não existir
                            for k, v in pairs(normalized) do
                                -- Para video: SEMPRE preserva o do template se existir (não faz merge)
                                if k == "video" then
                                    -- Se o template tem vídeo, preserva (não sobrescreve com módulo)
                                    if obj.weapon.video and trim(obj.weapon.video) ~= "" then
                                        -- Mantém o vídeo do template, não faz nada
                                    elseif v and v ~= "" then
                                        -- Só usa do módulo se template não tem
                                        obj.weapon.video = v
                                    end
                                    -- Para icon: preserva o do template se não for Nada.png
                                elseif k == "icon" then
                                    if (not obj.weapon.icon or obj.weapon.icon == "" or obj.weapon.icon == "Nada.png") and v and v ~= "" and v ~= "Nada.png" then
                                        obj.weapon.icon = v
                                    end
                                    -- Para outros campos: só faz merge se o campo NÃO EXISTE no template (nil)
                                    -- Se o campo existe no template (mesmo que vazio), preserva o do template
                                else
                                    -- IMPORTANTE: só faz merge se o campo é nil (não existe), não se está vazio
                                    if obj.weapon[k] == nil and v ~= nil and v ~= "" then
                                        obj.weapon[k] = v
                                    end
                                end
                            end
                            -- Sempre adiciona desc_i18n do módulo se não tiver no template
                            if normalized.desc_i18n and not obj.weapon.desc_i18n then
                                obj.weapon.desc_i18n = normalized.desc_i18n
                            end
                        else
                            obj.weapon = normalized
                        end
                        -- Adiciona campos descPt, descEn, etc. para compatibilidade
                        if obj.weapon.desc_i18n then
                            obj.weapon.descPt = obj.weapon.desc_i18n.pt
                            obj.weapon.descEn = obj.weapon.desc_i18n.en
                            obj.weapon.descEs = obj.weapon.desc_i18n.es
                            obj.weapon.descPl = obj.weapon.desc_i18n.pl
                        end
                    end
                    break
                end
            end
        end

        if obj.weapon and type(obj.weapon) == "table" and next(obj.weapon) == nil then
            obj.weapon = nil
        end
    end

    -- GARANTIA FINAL: Normaliza weapon antes de serializar
    if obj.weapon then
        ensureWeaponHydrated(obj)
        -- Se weapon ainda é string ou inválido, tenta parsear
        if obj.weapon and type(obj.weapon) ~= "table" then
            local parsed = parseWeaponString(obj.weapon)
            if parsed and next(parsed) ~= nil then
                obj.weapon = parsed
            else
                obj.weapon = nil
            end
        end
        -- Remove weapon se estiver vazio
        if obj.weapon and type(obj.weapon) == "table" and next(obj.weapon) == nil then
            obj.weapon = nil
        end
    end

    return mw.text.jsonEncode(obj)
end

return p