Mudanças entre as edições de "Módulo:Info.Skills"
Ir para navegação
Ir para pesquisar
m (fix2 chopp) Etiqueta: Revertido |
m (fix3 chopper) Etiqueta: Revertido |
||
| Linha 157: | Linha 157: | ||
if not data or not data.skills then return nil end | if not data or not data.skills then return nil end | ||
for _, sk in pairs(data.skills) do | for _, sk in pairs(data.skills) do | ||
if type(sk) == "table" | if type(sk) == "table" then | ||
-- Aceita tanto subskills quanto subs | |||
local subMap = sk.subskills or sk.subs | |||
if type(subMap) == "table" then | |||
local sub = subMap[skillName] | |||
if sub and type(sub.desc) == "table" then | |||
return sub.desc | |||
end | |||
end | end | ||
end | end | ||
| Linha 362: | Linha 366: | ||
end | end | ||
end | end | ||
end | |||
-- Função utilitária para expandir subs/subskills (map) + suborder em array ordenado | |||
-- Aceita tanto 'subs' quanto 'subskills' e converte map em array usando suborder | |||
local function expandOrderedSubs(node, data) | |||
if not node or type(node) ~= "table" then return nil end | |||
-- Pega suborder e o mapa de subskills/subs | |||
local suborder = node.suborder | |||
local subMap = node.subskills or node.subs | |||
-- Se não tem suborder ou subMap, retorna nil | |||
if not suborder or type(suborder) ~= "table" or #suborder == 0 then | |||
return nil | |||
end | |||
if not subMap or type(subMap) ~= "table" then | |||
return nil | |||
end | |||
-- Converte map em array ordenado | |||
local subsArr = {} | |||
for _, name in ipairs(suborder) do | |||
local raw = subMap[name] | |||
if type(raw) == "table" then | |||
-- Cria objeto da subskill | |||
local subObj = { | |||
name = name, | |||
n = name | |||
} | |||
-- Copia descrição (pode ser desc ou desc_i18n) | |||
if type(raw.desc) == "table" then | |||
local desc_i18n = {} | |||
local langs = { "pt", "en", "es", "pl" } | |||
for _, code in ipairs(langs) do | |||
local d = raw.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 | |||
-- Copia atributos se existirem | |||
if raw.icon and trim(raw.icon) ~= "" then subObj.icon = raw.icon end | |||
if raw.level and trim(tostring(raw.level)) ~= "" then subObj.level = tostring(raw.level) end | |||
if raw.video and trim(raw.video) ~= "" then subObj.video = raw.video end | |||
if raw.energy ~= nil then subObj.energy = raw.energy end | |||
if raw.powerpve ~= nil then subObj.powerpve = raw.powerpve end | |||
if raw.powerpvp ~= nil then subObj.powerpvp = raw.powerpvp end | |||
if raw.cooldown ~= nil then subObj.cooldown = raw.cooldown end | |||
if raw.back then subObj.back = raw.back end | |||
if raw.flags and type(raw.flags) == "table" then subObj.flags = raw.flags end | |||
if raw.weapon and type(raw.weapon) == "table" then | |||
local weaponObj = normalizeWeaponTable(raw.weapon) | |||
if weaponObj then subObj.weapon = weaponObj end | |||
end | |||
-- RECURSIVO: expande sub-subskills se existirem | |||
local nestedSubs = expandOrderedSubs(raw, data) | |||
if nestedSubs and #nestedSubs > 0 then | |||
subObj.subs = nestedSubs | |||
end | |||
table.insert(subsArr, subObj) | |||
end | |||
end | |||
return #subsArr > 0 and subsArr or nil | |||
end | end | ||
| Linha 562: | Linha 641: | ||
for _, skillKey in ipairs(data.order or {}) do | for _, skillKey in ipairs(data.order or {}) do | ||
local skillData = (data.skills or {})[skillKey] | local skillData = (data.skills or {})[skillKey] | ||
if type(skillData) == "table" | if type(skillData) == "table" then | ||
local moduleSub = | local subMap = skillData.subskills or skillData.subs | ||
local moduleSub = subMap and subMap[subName] or nil | |||
if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then | if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then | ||
local moduleWeapon = normalizeWeaponTable(moduleSub.weapon) | local moduleWeapon = normalizeWeaponTable(moduleSub.weapon) | ||
| Linha 755: | Linha 835: | ||
for _, key in ipairs(data.order or {}) do | for _, key in ipairs(data.order or {}) do | ||
local skillData = (data.skills or {})[key] | local skillData = (data.skills or {})[key] | ||
if type(skillData) == "table" | if type(skillData) == "table" then | ||
local moduleSub = | local subMap = skillData.subskills or skillData.subs | ||
local moduleSub = subMap and subMap[subName] or nil | |||
if moduleSub and type(moduleSub.desc) == "table" then | if moduleSub and type(moduleSub.desc) == "table" then | ||
foundDesc = moduleSub.desc | foundDesc = moduleSub.desc | ||
| Linha 790: | Linha 871: | ||
for _, skillKey in ipairs(data.order or {}) do | for _, skillKey in ipairs(data.order or {}) do | ||
local skillData = (data.skills or {})[skillKey] | local skillData = (data.skills or {})[skillKey] | ||
if type(skillData) == "table" | if type(skillData) == "table" then | ||
local moduleSub = | local subMap = skillData.subskills or skillData.subs | ||
local moduleSub = subMap and subMap[subName] or nil | |||
if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then | if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then | ||
local moduleWeapon = normalizeWeaponTable(moduleSub.weapon) | local moduleWeapon = normalizeWeaponTable(moduleSub.weapon) | ||
| Linha 873: | Linha 955: | ||
local key = order[m] or "" | local key = order[m] or "" | ||
local sk = (data and data.skills and data.skills[key]) or {} | local sk = (data and data.skills and data.skills[key]) or {} | ||
-- Usa expandOrderedSubs para converter subs/subskills (map) + suborder em array | |||
local expanded = expandOrderedSubs(sk, data) | |||
if expanded and #expanded > 0 then | |||
for _, subObj in ipairs(expanded) do | |||
-- Se tem inherit, busca atributos da skill principal referenciada | |||
local inheritIdx = subObj.inherit | |||
if inheritIdx and tonumber(inheritIdx) then | |||
local mainSkillMeta = fetchSkillMeta(char, inheritIdx) | |||
if mainSkillMeta then | |||
if not subObj.icon or trim(subObj.icon) == "" then | |||
subObj.icon = mainSkillMeta.icon or "" | |||
end | end | ||
if not subObj.level or trim(subObj.level) == "" then | |||
subObj.level = mainSkillMeta.level or "" | |||
end | end | ||
if subObj.energy == nil then subObj.energy = mainSkillMeta.energy end | |||
if subObj.powerpve == nil then subObj.powerpve = mainSkillMeta.powerpve end | |||
if subObj.powerpvp == nil then subObj.powerpvp = mainSkillMeta.powerpvp end | |||
if subObj.cooldown == nil then subObj.cooldown = mainSkillMeta.cooldown end | |||
-- Se não tem descrição própria, herda da skill principal | |||
if not subObj.desc_i18n and not subObj.descPt then | |||
subObj.desc_i18n = { | |||
pt = mainSkillMeta.descPt, | |||
en = mainSkillMeta.descEn, | |||
es = mainSkillMeta.descEs, | |||
pl = mainSkillMeta.descPl | |||
} | |||
subObj.descPt = mainSkillMeta.descPt | |||
subObj.descEn = mainSkillMeta.descEn | |||
subObj.descEs = mainSkillMeta.descEs | |||
subObj.descPl = mainSkillMeta.descPl | |||
end | end | ||
end | end | ||
end | end | ||
table.insert(subsArr, subObj) | |||
end | end | ||
end | end | ||
| Linha 996: | Linha 1 007: | ||
for _, skillKey in ipairs(data.order or {}) do | for _, skillKey in ipairs(data.order or {}) do | ||
local skillData = (data.skills or {})[skillKey] | local skillData = (data.skills or {})[skillKey] | ||
if type(skillData) == "table" | if type(skillData) == "table" then | ||
local moduleSub = | local subMap = skillData.subskills or skillData.subs | ||
local moduleSub = subMap and subMap[subName] or nil | |||
if moduleSub then | if moduleSub then | ||
-- DEBUG: Log temporário | -- DEBUG: Log temporário | ||
| Linha 1 043: | Linha 1 055: | ||
for _, skillKey in ipairs(data.order or {}) do | for _, skillKey in ipairs(data.order or {}) do | ||
local skillData = (data.skills or {})[skillKey] | local skillData = (data.skills or {})[skillKey] | ||
if type(skillData) == "table" | if type(skillData) == "table" then | ||
local moduleSub = | local subMap = skillData.subskills or skillData.subs | ||
local moduleSub = subMap and subMap[subName] or nil | |||
if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then | if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then | ||
local moduleWeapon = normalizeWeaponTable(moduleSub.weapon) | local moduleWeapon = normalizeWeaponTable(moduleSub.weapon) | ||
| Linha 1 112: | Linha 1 125: | ||
for _, skillKey in ipairs(data.order or {}) do | for _, skillKey in ipairs(data.order or {}) do | ||
local skillData = (data.skills or {})[skillKey] | local skillData = (data.skills or {})[skillKey] | ||
if type(skillData) == "table" | if type(skillData) == "table" then | ||
local subMap = skillData.subskills or skillData.subs | |||
-- Tenta match exato primeiro | -- Tenta match exato primeiro | ||
local moduleSub = | local moduleSub = subMap and subMap[subName] or nil | ||
-- Se não encontrou, tenta case-insensitive | -- Se não encontrou, tenta case-insensitive | ||
if not moduleSub then | if not moduleSub and subMap then | ||
for k, v in pairs( | for k, v in pairs(subMap) do | ||
if type(k) == "string" and k:lower() == subName:lower() then | if type(k) == "string" and k:lower() == subName:lower() then | ||
moduleSub = v | moduleSub = v | ||
| Linha 1 199: | Linha 1 213: | ||
end | end | ||
-- suborder do módulo | -- suborder do módulo e expansão de subs/subskills | ||
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 1 205: | Linha 1 219: | ||
local key = order[m] or "" | local key = order[m] or "" | ||
local sk = (data and data.skills and data.skills[key]) or {} | local sk = (data and data.skills and data.skills[key]) or {} | ||
if type(sk) == "table" | if type(sk) == "table" then | ||
if type(sk.suborder) == "table" then | |||
obj.suborder = sk.suborder | |||
end | |||
-- IMPORTANTE: Expande subs/subskills (map) + suborder em array ordenado | |||
-- Isso permite que Character.Generate injete data-subs corretamente | |||
local expanded = expandOrderedSubs(sk, data) | |||
if expanded and #expanded > 0 then | |||
obj.subs = expanded | |||
end | |||
end | end | ||
end | end | ||
| Linha 1 498: | Linha 1 520: | ||
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] | ||
if type(sk) == "table" | if type(sk) == "table" then | ||
local sub = | local subMap = sk.subskills or sk.subs | ||
local sub = subMap and subMap[n] or nil | |||
if sub and type(sub.desc) == "table" then | if sub and type(sub.desc) == "table" then | ||
foundDesc = sub.desc | foundDesc = sub.desc | ||
| Linha 1 616: | Linha 1 639: | ||
for _, skillKey in ipairs(data.order or {}) do | for _, skillKey in ipairs(data.order or {}) do | ||
local skillData = (data.skills or {})[skillKey] | local skillData = (data.skills or {})[skillKey] | ||
if type(skillData) == "table" | if type(skillData) == "table" then | ||
local moduleSub = | local subMap = skillData.subskills or skillData.subs | ||
local moduleSub = subMap and subMap[subName] or nil | |||
if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then | if moduleSub and type(moduleSub.weapon) == "table" and next(moduleSub.weapon) ~= nil then | ||
local weaponObj = normalizeWeaponTable(moduleSub.weapon) | local weaponObj = normalizeWeaponTable(moduleSub.weapon) | ||
| Linha 1 643: | Linha 1 667: | ||
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] | ||
if type(sk) == "table" | if type(sk) == "table" then | ||
local sub = | local subMap = sk.subskills or sk.subs | ||
local sub = subMap and subMap[n] or nil | |||
if sub and type(sub.weapon) == "table" then | if sub and type(sub.weapon) == "table" then | ||
local normalized = normalizeWeaponTable(sub.weapon) | local normalized = normalizeWeaponTable(sub.weapon) | ||
Edição das 02h04min de 29 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
-- Helper para pegar o primeiro valor não-nil e não-vazio após trim
-- IMPORTANTE: Em Lua, string vazia "" é truthy; não use `a.x or a.Y` quando x pode ser ''
-- Retorna o primeiro valor que não seja nil e não seja string vazia/whitespace após trim
local function firstNonEmpty(...)
for _, val in ipairs({ ... }) do
if val ~= nil and trim(tostring(val)) ~= "" then
return trim(tostring(val))
end
end
return nil
end
-- ===== 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
-- IMPORTANTE: Só faz herança se tem refM/M/m explícito (não usa name/n como ref)
-- Subskills normais (sem |M=) não devem passar por herança de meta
local ref = firstNonEmpty(sub.refM, sub.M, sub.m)
if not 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)
-- IMPORTANTE: Vídeo NUNCA é herdado
-- Se sub.video não existe, NÃO cria a chave (deixa como nil)
-- Isso permite ao JS distinguir entre "não definido" (não herda) vs "definido como vazio" (usa vazio)
-- O vídeo já foi copiado nas linhas 81-83 se existir, então não fazemos nada aqui
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" then
-- Aceita tanto subskills quanto subs
local subMap = sk.subskills or sk.subs
if type(subMap) == "table" then
local sub = subMap[skillName]
if sub and type(sub.desc) == "table" then
return sub.desc
end
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
-- Função utilitária para expandir subs/subskills (map) + suborder em array ordenado
-- Aceita tanto 'subs' quanto 'subskills' e converte map em array usando suborder
local function expandOrderedSubs(node, data)
if not node or type(node) ~= "table" then return nil end
-- Pega suborder e o mapa de subskills/subs
local suborder = node.suborder
local subMap = node.subskills or node.subs
-- Se não tem suborder ou subMap, retorna nil
if not suborder or type(suborder) ~= "table" or #suborder == 0 then
return nil
end
if not subMap or type(subMap) ~= "table" then
return nil
end
-- Converte map em array ordenado
local subsArr = {}
for _, name in ipairs(suborder) do
local raw = subMap[name]
if type(raw) == "table" then
-- Cria objeto da subskill
local subObj = {
name = name,
n = name
}
-- Copia descrição (pode ser desc ou desc_i18n)
if type(raw.desc) == "table" then
local desc_i18n = {}
local langs = { "pt", "en", "es", "pl" }
for _, code in ipairs(langs) do
local d = raw.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
-- Copia atributos se existirem
if raw.icon and trim(raw.icon) ~= "" then subObj.icon = raw.icon end
if raw.level and trim(tostring(raw.level)) ~= "" then subObj.level = tostring(raw.level) end
if raw.video and trim(raw.video) ~= "" then subObj.video = raw.video end
if raw.energy ~= nil then subObj.energy = raw.energy end
if raw.powerpve ~= nil then subObj.powerpve = raw.powerpve end
if raw.powerpvp ~= nil then subObj.powerpvp = raw.powerpvp end
if raw.cooldown ~= nil then subObj.cooldown = raw.cooldown end
if raw.back then subObj.back = raw.back end
if raw.flags and type(raw.flags) == "table" then subObj.flags = raw.flags end
if raw.weapon and type(raw.weapon) == "table" then
local weaponObj = normalizeWeaponTable(raw.weapon)
if weaponObj then subObj.weapon = weaponObj end
end
-- RECURSIVO: expande sub-subskills se existirem
local nestedSubs = expandOrderedSubs(raw, data)
if nestedSubs and #nestedSubs > 0 then
subObj.subs = nestedSubs
end
table.insert(subsArr, subObj)
end
end
return #subsArr > 0 and subsArr or nil
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 = nil, -- MUDANÇA: nil em vez de "" para permitir herança correta
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)
-- IMPORTANTE: Usa firstNonEmpty porque string vazia "" é truthy em Lua
local ref = firstNonEmpty(sk.refM, sk.M, sk.m, sk.refS, sk.S, 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]
-- IMPORTANTE: Define refM se o JSON tem refM/M/m (para que inheritSubFromMeta funcione corretamente)
-- Usa firstNonEmpty porque string vazia "" é truthy em Lua
local refMValue = firstNonEmpty(sk.refM, sk.M, sk.m)
if refMValue then
existing.refM = refMValue
end
-- Para video: SEMPRE copia do JSON (mesmo que seja string vazia)
-- Se o JSON tem o campo video, copia (pode ser string vazia se não foi especificado)
-- Se não existe no JSON, existing.video permanece como está
if sk.video ~= nil then
existing.video = sk.video
end
-- IMPORTANTE: Se o campo não existe no JSON (nil), não sobrescreve existing.video
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" then
local subMap = skillData.subskills or skillData.subs
local moduleSub = subMap and subMap[subName] or nil
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
-- IMPORTANTE: Usa firstNonEmpty porque string vazia "" é truthy em Lua
local ref = firstNonEmpty(sk.refM, sk.M, sk.m, sk.refS, sk.S, 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, mas NÃO sobrescreve vídeo explícito
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
-- Vídeo NUNCA é herdado, apenas preserva o que está na subskill
-- sk.video permanece como está (ou nil/"" se não especificado)
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
-- IMPORTANTE: Usa firstNonEmpty porque string vazia "" é truthy em Lua
local refMExplicit = firstNonEmpty(sk.refM, sk.M, sk.m)
if 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, mas NÃO sobrescreve vídeo explícito
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
-- Vídeo NUNCA é herdado, apenas preserva o que está na subskill
-- sk.video permanece como está (ou nil/"" se não especificado)
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
-- Para subskills normais (sem ref), busca descrição do módulo se não tiver
if not ref or trim(tostring(ref)) == "" then
local subName = sk.name or sk.n or ""
if subName ~= "" and (not sk.desc_i18n and not sk.descPt) then
-- Busca descrição do módulo
local foundDesc = nil
for _, key in ipairs(data.order or {}) do
local skillData = (data.skills or {})[key]
if type(skillData) == "table" then
local subMap = skillData.subskills or skillData.subs
local moduleSub = subMap and subMap[subName] or nil
if moduleSub and type(moduleSub.desc) == "table" then
foundDesc = moduleSub.desc
break
end
end
end
if not foundDesc then
foundDesc = resolveDescWithInheritance(data, subName, 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
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
}
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" then
local subMap = skillData.subskills or skillData.subs
local moduleSub = subMap and subMap[subName] or nil
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
-- IMPORTANTE: Processa subskills aninhadas se existirem no JSON
-- Isso permite que subskills dentro de subskills sejam processadas corretamente
if type(sk.subs) == "string" and trim(sk.subs) ~= "" then
-- Se subs é uma string (JSON não parseado), tenta parsear
local nestedChunks = collectJsonObjects(sk.subs)
if #nestedChunks > 0 then
local nestedArr = {}
for _, nestedChunk in ipairs(nestedChunks) do
local ok2, nestedSub = pcall(mw.text.jsonDecode, nestedChunk)
if ok2 and type(nestedSub) == "table" then
ensureWeaponHydrated(nestedSub)
normalizeSubTree(nestedSub)
table.insert(nestedArr, nestedSub)
end
end
if #nestedArr > 0 then
sk.subs = nestedArr
end
end
elseif type(sk.subs) == "table" then
-- Se já é uma tabela, processa recursivamente
for _, nestedSub in ipairs(sk.subs) do
if type(nestedSub) == "table" then
ensureWeaponHydrated(nestedSub)
normalizeSubTree(nestedSub)
end
end
end
-- Garante que weapon está normalizado antes de adicionar
ensureWeaponHydrated(sk)
normalizeSubTree(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 {}
-- Usa expandOrderedSubs para converter subs/subskills (map) + suborder em array
local expanded = expandOrderedSubs(sk, data)
if expanded and #expanded > 0 then
for _, subObj in ipairs(expanded) do
-- Se tem inherit, busca atributos da skill principal referenciada
local inheritIdx = subObj.inherit
if inheritIdx and tonumber(inheritIdx) then
local mainSkillMeta = fetchSkillMeta(char, inheritIdx)
if mainSkillMeta then
if not subObj.icon or trim(subObj.icon) == "" then
subObj.icon = mainSkillMeta.icon or ""
end
if not subObj.level or trim(subObj.level) == "" then
subObj.level = mainSkillMeta.level or ""
end
if subObj.energy == nil then subObj.energy = mainSkillMeta.energy end
if subObj.powerpve == nil then subObj.powerpve = mainSkillMeta.powerpve end
if subObj.powerpvp == nil then subObj.powerpvp = mainSkillMeta.powerpvp end
if subObj.cooldown == nil then subObj.cooldown = mainSkillMeta.cooldown end
-- Se não tem descrição própria, herda da skill principal
if not subObj.desc_i18n and not subObj.descPt then
subObj.desc_i18n = {
pt = mainSkillMeta.descPt,
en = mainSkillMeta.descEn,
es = mainSkillMeta.descEs,
pl = mainSkillMeta.descPl
}
subObj.descPt = mainSkillMeta.descPt
subObj.descEn = mainSkillMeta.descEn
subObj.descEs = mainSkillMeta.descEs
subObj.descPl = mainSkillMeta.descPl
end
end
end
table.insert(subsArr, subObj)
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" then
local subMap = skillData.subskills or skillData.subs
local moduleSub = subMap and subMap[subName] or nil
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" then
local subMap = skillData.subskills or skillData.subs
local moduleSub = subMap and subMap[subName] or nil
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)
-- IMPORTANTE: NÃO força video = "" se não existe
-- Se video == nil, campo não existe no JSON = vídeo não foi especificado pelo editor
-- Isso permite ao JS distinguir entre "não definido" vs "definido como vazio"
-- 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" then
local subMap = skillData.subskills or skillData.subs
-- Tenta match exato primeiro
local moduleSub = subMap and subMap[subName] or nil
-- Se não encontrou, tenta case-insensitive
if not moduleSub and subMap then
for k, v in pairs(subMap) 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
-- IMPORTANTE: NÃO força video = "" se não existe
-- Se video == nil, campo não existe no JSON = vídeo não foi especificado pelo editor
-- Isso permite ao JS distinguir entre "não definido" vs "definido como vazio"
end
end
end
-- suborder do módulo e expansão de subs/subskills
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" then
if type(sk.suborder) == "table" then
obj.suborder = sk.suborder
end
-- IMPORTANTE: Expande subs/subskills (map) + suborder em array ordenado
-- Isso permite que Character.Generate injete data-subs corretamente
local expanded = expandOrderedSubs(sk, data)
if expanded and #expanded > 0 then
obj.subs = expanded
end
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
-- Weaponicon (ícone do botão de toggle)
if trim(a.weaponicon or "") ~= "" then
obj.weaponicon = trim(a.weaponicon)
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 "")
-- IMPORTANTE: Usa firstNonEmpty porque template sempre passa |m={{{m|}}} e |M={{{M|}}}
-- Se usuário especifica |M=1, então m chega como "" (string vazia é truthy em Lua)
-- Por isso não podemos usar `a.m or a.M` - retornaria "" em vez de "1"
local refM = firstNonEmpty(a.m, a.M)
local refS = firstNonEmpty(a.S, a.s)
-- Prioriza |M= sobre |S= para herança
local ref = 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
-- IMPORTANTE: Vídeo só é incluído se foi explicitamente especificado no template
-- Se não foi especificado, campo não existe (nil) = não herda
-- Se foi especificado (mesmo que vazio), campo existe = decisão do editor
local videoValue = nil
if a.video ~= nil then -- Campo foi passado no template (mesmo que string vazia)
videoValue = trim(a.video or "")
end
local obj = {
name = pick(a.n, inheritedName),
icon = pick(a.icon, meta.icon),
level = pick(a.level, meta.level),
video = videoValue, -- nil se não especificado, string (pode ser vazia) se especificado
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)
-- Weaponicon (ícone do botão de toggle)
if trim(a.weaponicon or "") ~= "" then
obj.weaponicon = trim(a.weaponicon)
end
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
-- IMPORTANTE: Vídeo só é incluído se foi explicitamente especificado no template
local videoValue = nil
if a.video ~= nil then
videoValue = trim(a.video or "")
end
local obj = {
refM = ref,
name = skillName,
n = skillName,
icon = trim(a.icon or ""),
level = trim(a.level or ""),
video = videoValue, -- nil se não especificado, string (pode ser vazia) se especificado
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)
-- Weaponicon (ícone do botão de toggle)
if trim(a.weaponicon or "") ~= "" then
obj.weaponicon = trim(a.weaponicon)
end
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
-- IMPORTANTE: Vídeo sempre presente (string vazia se não especificado)
-- IMPORTANTE: Vídeo só é incluído se foi explicitamente especificado no template
local videoValue = nil
if a.video ~= nil then
videoValue = trim(a.video or "")
end
local obj = {
refM = refM,
icon = trim(a.icon or ""),
level = trim(a.level or ""),
video = videoValue, -- nil se não especificado, string (pode ser vazia) se especificado
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)
-- Weaponicon (ícone do botão de toggle)
if trim(a.weaponicon or "") ~= "" then
obj.weaponicon = trim(a.weaponicon)
end
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" then
local subMap = sk.subskills or sk.subs
local sub = subMap and subMap[n] or nil
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
-- IMPORTANTE: Vídeo só é incluído se foi explicitamente especificado no template
local videoValue = nil
if a.video ~= nil then
videoValue = trim(a.video or "")
end
local obj = {
name = n,
icon = trim(a.icon or ""),
level = trim(a.level or ""),
video = videoValue, -- nil se não especificado, string (pode ser vazia) se especificado
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 ""
-- IMPORTANTE: Se subsPacked contém templates não expandidos ({{Subskill}}),
-- tenta expandir recursivamente usando frame:preprocess
if frame and subsPacked ~= "" and subsPacked:match("{{") then
-- Se contém {{, pode conter templates não expandidos
-- Tenta expandir usando frame:preprocess para expandir todos os templates
subsPacked = frame:preprocess(subsPacked)
end
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)
-- IMPORTANTE: Processa subskills aninhadas se existirem no JSON
-- Isso permite que subskills dentro de subskills sejam processadas corretamente
if type(sub.subs) == "string" and trim(sub.subs) ~= "" then
-- Se subs é uma string (JSON não parseado), tenta parsear
local nestedChunks = collectJsonObjects(sub.subs)
if #nestedChunks > 0 then
local nestedArr = {}
for _, nestedChunk in ipairs(nestedChunks) do
local ok2, nestedSub = pcall(mw.text.jsonDecode, nestedChunk)
if ok2 and type(nestedSub) == "table" then
ensureWeaponHydrated(nestedSub)
normalizeSubTree(nestedSub)
table.insert(nestedArr, nestedSub)
end
end
if #nestedArr > 0 then
sub.subs = nestedArr
end
end
elseif type(sub.subs) == "table" then
-- Se já é uma tabela, processa recursivamente
for _, nestedSub in ipairs(sub.subs) do
if type(nestedSub) == "table" then
ensureWeaponHydrated(nestedSub)
normalizeSubTree(nestedSub)
end
end
end
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" then
local subMap = skillData.subskills or skillData.subs
local moduleSub = subMap and subMap[subName] or nil
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" then
local subMap = sk.subskills or sk.subs
local sub = subMap and subMap[n] or nil
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