Mudanças entre as edições de "Widget:Character.Skills"
Ir para navegação
Ir para pesquisar
m |
m |
||
| Linha 1: | Linha 1: | ||
<!-- | <!-- SUBSKILLS SYSTEM --> | ||
<script> | <script> | ||
(function () { | (function () { | ||
const | const api = (window.__subskills ||= {}); | ||
const subCache = new Map(); | |||
const imagePreloadCache = new Map(); | |||
let subRail, subBar, spacer; | |||
const | |||
const imagePreloadCache | |||
// | // Cache das skills principais (capturado na carga da página) | ||
let cachedMainSkills = null; | |||
function | // ===== HERANÇA DE ATRIBUTOS: busca dados das skills principais ===== | ||
function getMainSkillsMap() { | |||
// Retorna cache se já foi construído E tem dados | |||
if (cachedMainSkills && cachedMainSkills.byIndex.size > 0) { | |||
if ( | return cachedMainSkills; | ||
return | } | ||
const maps = { | |||
byName: new Map(), | |||
byIndex: new Map() | |||
}; | |||
// Busca skills com data-index (skills principais, não subskills) | |||
const | const icons = document.querySelectorAll('.icon-bar .skill-icon[data-index][data-nome]'); | ||
icons.forEach(icon => { | |||
const name = (icon.dataset.nome || '').trim(); | |||
if (!name) return; | |||
// Extrai atributos do data-atr (formato: "pve, pvp, energy, cd") | |||
const atrRaw = icon.dataset.atr || ''; | |||
const parts = atrRaw.split(',').map(x => (x || '').trim()); | |||
const powerpve = parts[0] && parts[0] !== '-' ? parts[0] : ''; | |||
const powerpvp = parts[1] && parts[1] !== '-' ? parts[1] : ''; | |||
const energy = parts[2] && parts[2] !== '-' ? parts[2] : ''; | |||
const cooldown = parts[3] && parts[3] !== '-' ? parts[3] : ''; | |||
// Nome original do arquivo de ícone (armazenado no dataset pelo widget de skills) | |||
let iconFile = (icon.dataset.iconFile || '').trim(); | let iconFile = (icon.dataset.iconFile || '').trim(); | ||
if (!iconFile) { | if (!iconFile) { | ||
| Linha 178: | Linha 44: | ||
iconFile = iconMatch ? decodeURIComponent(iconMatch[1]) : ''; | iconFile = iconMatch ? decodeURIComponent(iconMatch[1]) : ''; | ||
} | } | ||
// Nome original do arquivo de vídeo (caso exista) | |||
let videoFile = (icon.dataset.videoFile || '').trim(); | let videoFile = (icon.dataset.videoFile || '').trim(); | ||
if (!videoFile) { | if (!videoFile) { | ||
const videoUrl = icon.dataset.video || ''; | |||
const videoMatch = videoUrl.match(/FilePath\/([^&?]+)/); | |||
videoFile = videoMatch ? decodeURIComponent(videoMatch[1]) : ''; | |||
} | } | ||
const | |||
const index = (icon.dataset.index || '').trim(); | |||
icon: iconFile | const data = { | ||
name: name, // Inclui o nome para herança completa | |||
icon: iconFile, | |||
level: icon.dataset.level || '', | level: icon.dataset.level || '', | ||
video: videoFile | video: videoFile, | ||
powerpve: | powerpve: powerpve, | ||
powerpvp: | powerpvp: powerpvp, | ||
cooldown: cooldown, | |||
cooldown: | energy: energy | ||
}; | }; | ||
// Mantém descrições caso precise como fallback extra | |||
if ( | if (icon.dataset.descPt) data.descPt = icon.dataset.descPt; | ||
if (icon.dataset.descEn) data.descEn = icon.dataset.descEn; | |||
if (icon.dataset.descEs) data.descEs = icon.dataset.descEs; | |||
if (icon.dataset.descPl) data.descPl = icon.dataset.descPl; | |||
maps.byName.set(name, data); | |||
if (index) { | |||
// Guarda tanto como string quanto como número para compatibilidade | |||
maps.byIndex.set(index, data); | |||
maps.byIndex.set(parseInt(index, 10), data); | |||
} | } | ||
}); | }); | ||
return | // Cacheia para uso futuro (importante: as skills principais não mudam) | ||
cachedMainSkills = maps; | |||
return maps; | |||
} | } | ||
// Aplica herança COMPLETA: se subskill só tem refM, busca TUDO da skill principal | |||
function applyInheritance(sub, mainSkills) { | |||
let name = (sub.name || sub.n || '').trim(); | let name = (sub.name || sub.n || '').trim(); | ||
const refIndex = ((sub.refM || sub.m || sub.M || '') + '').trim(); | |||
// | let main = null; | ||
if ( | // Tenta como string | ||
main = | if (refIndex && mainSkills.byIndex.has(refIndex)) { | ||
main = mainSkills.byIndex.get(refIndex); | |||
} | } | ||
// | // Tenta como número | ||
if (!main && refIndex) { | if (!main && refIndex) { | ||
const numIndex = parseInt(refIndex, 10); | |||
if (!isNaN(numIndex) && mainSkills.byIndex.has(numIndex)) { | |||
main = mainSkills.byIndex.get(numIndex); | |||
} | |||
} | } | ||
if (!main && name && mainSkills.byName.has(name)) { | |||
if (!main && name) { | main = mainSkills.byName.get(name); | ||
main = | |||
} | } | ||
// Se não tem main skill para herdar, retorna como está | |||
if (!main) { | if (!main) { | ||
return sub; | return sub; | ||
} | } | ||
// Se não tem nome mas tem refM, herda o nome da skill principal | |||
if (!name && main.name) { | if (!name && refIndex && main.name) { | ||
name = main.name; | name = main.name; | ||
} | } | ||
return { | |||
...sub, | |||
name: name || main.name || sub.name, | |||
icon: (sub.icon && sub.icon !== 'Nada.png' && sub.icon !== '') ? sub.icon : (main.icon || 'Nada.png'), | |||
level: sub.level || main.level, | |||
video: sub.video || main.video, | |||
powerpve: sub.powerpve || main.powerpve, | |||
powerpvp: sub.powerpvp || main.powerpvp, | |||
cooldown: sub.cooldown || main.cooldown, | |||
energy: sub.energy || main.energy, | |||
descPt: sub.descPt || (sub.desc_i18n?.pt) || main.descPt, | |||
descEn: sub.descEn || (sub.desc_i18n?.en) || main.descEn, | |||
descEs: sub.descEs || (sub.desc_i18n?.es) || main.descEs, | |||
descPl: sub.descPl || (sub.desc_i18n?.pl) || main.descPl | |||
}; | |||
} | |||
function filePathURL(fileName) { | |||
const f = encodeURIComponent((fileName || 'Nada.png').replace(/^Arquivo:|^File:/, '')); | |||
const base = (window.mw && mw.util && typeof mw.util.wikiScript === 'function') | |||
? mw.util.wikiScript() | |||
: (window.mw && mw.config ? (mw.config.get('wgScript') || '/index.php') : '/index.php'); | |||
return `${base}?title=Especial:FilePath/${f}`; | |||
} | |||
} | } | ||
function | function normalizeFileURL(raw, fallback = '') { | ||
if (! | if (!raw) return fallback; | ||
const val = String(raw).trim(); | |||
if (!val) return fallback; | |||
if (/^(https?:)?\/\//i.test(val) || val.startsWith('data:') || val.includes('Especial:FilePath/')) { | |||
return val; | |||
} | |||
return | return filePathURL(val); | ||
}); | |||
} | } | ||
function | function preloadImage(iconFile) { | ||
const url = filePathURL(iconFile || 'Nada.png'); | |||
if (imagePreloadCache.has(url)) { | |||
return imagePreloadCache.get(url); | |||
} | |||
const promise = new Promise((resolve, reject) => { | |||
return | |||
} | |||
const img = new Image(); | const img = new Image(); | ||
img. | img.onload = () => resolve(url); | ||
img. | img.onerror = () => resolve(url); | ||
img.src = url; | img.src = url; | ||
}); | }); | ||
imagePreloadCache.set(url, promise); | |||
return promise; | |||
return | |||
} | } | ||
function getLabels() { | |||
const skillsRoot = document.getElementById('skills'); | |||
const i18nMap = skillsRoot ? JSON.parse(skillsRoot.dataset.i18nAttrs || '{}') : {}; | |||
const raw = (document.documentElement.lang || skillsRoot?.dataset.i18nDefault || 'pt').toLowerCase(); | |||
const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt'); | |||
return i18nMap[lang] || i18nMap.pt || { | |||
cooldown: 'Recarga', | |||
energy_gain: 'Ganho de energia', | |||
energy_cost: 'Custo de energia', | |||
power: 'Poder', | |||
power_pvp: 'Poder PvP', | |||
level: 'Nível' | |||
}; | |||
} | |||
} | } | ||
// Verifica se o modo weapon está ativo | |||
function isWeaponModeOn() { | |||
function | |||
try { | try { | ||
return localStorage.getItem('glaWeaponEnabled') === '1'; | |||
} catch ( | } catch (e) { | ||
return false; | |||
} | } | ||
} | } | ||
function | |||
// Retorna os atributos corretos (weapon ou normal) | |||
function getEffectiveAttrs(s) { | |||
const weaponOn = isWeaponModeOn(); | |||
if (weaponOn && s.weapon) { | |||
return { | |||
powerpve: s.weapon.powerpve || s.powerpve, | |||
powerpvp: s.weapon.powerpvp || s.powerpvp, | |||
energy: s.weapon.energy || s.energy, | |||
cooldown: s.weapon.cooldown || s.cooldown | |||
}; | |||
} | } | ||
return { | |||
powerpve: s.powerpve, | |||
powerpvp: s.powerpvp, | |||
energy: s.energy, | |||
cooldown: s.cooldown | |||
}; | |||
} | } | ||
function | |||
// Retorna a descrição correta (weapon ou normal) | |||
// Aceita tanto desc_i18n quanto desc para compatibilidade | |||
function getEffectiveDesc(s) { | |||
const weaponOn = isWeaponModeOn(); | |||
const raw = (document.documentElement.lang || 'pt').toLowerCase(); | |||
const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt'); | |||
// Para weapon: aceita tanto desc_i18n quanto desc | |||
if (weaponOn && s.weapon) { | |||
const wDesc = s.weapon.desc_i18n || s.weapon.desc; | |||
if (wDesc) { | |||
return wDesc[lang] || wDesc.pt || wDesc.en || ''; | |||
} | } | ||
} | } | ||
if ( | // Para descrição base: aceita tanto desc_i18n quanto desc | ||
const base = s.desc_i18n || s.desc; | |||
if (base) { | |||
return base[lang] || base.pt || base.en || ''; | |||
} | } | ||
// Fallback para campos individuais (compatibilidade) | |||
const descI18n = { | |||
pt: s.descPt || '', | |||
} | en: s.descEn || '', | ||
function | es: s.descEs || '', | ||
if ( | pl: s.descPl || '' | ||
}; | |||
return descI18n[lang] || descI18n.pt || ''; | |||
} | |||
// Retorna o vídeo correto (weapon ou normal) | |||
function getEffectiveVideo(s) { | |||
const weaponOn = isWeaponModeOn(); | |||
if (weaponOn && s.weapon && s.weapon.video && s.weapon.video.trim() !== '') { | |||
return s.weapon.video; | |||
} | } | ||
return s.video || ''; | |||
} | |||
function renderSubAttrs(s, L) { | |||
const chip = (label, val) => (val ? `<div class="attr-row"><span class="attr-label">${label}</span><span class="attr-value">${val}</span></div>` : ''); | |||
const pve = (s.powerpve || '').toString().trim(); | |||
const pvp = (s.powerpvp || '').toString().trim(); | |||
const en = (s.energy || '').toString().trim(); | |||
const cd = (s.cooldown || '').toString().trim(); | |||
const rows = [ | |||
cd ? chip(L.cooldown, cd) : '', | |||
en ? chip((en.startsWith('-') ? L.energy_cost : L.energy_gain), en.startsWith('-') ? en.replace(/^-/, '') : en.replace(/^\+?/, '')) : '', | |||
pve ? chip(L.power, pve) : '', | |||
pvp ? chip(L.power_pvp, pvp) : '', | |||
].filter(Boolean); | |||
return rows.length ? `<div class="attr-list">${rows.join('')}</div>` : ''; | |||
} | } | ||
function | function renderFlagsRow(flags) { | ||
const map = { | |||
aggro: 'Enemyaggro-icon.png', | |||
bridge: 'Bridgemaker-icon.png', | |||
wall: 'Destroywall-icon.png', | |||
quickcast: 'Quickcast-icon.png' | |||
}; | |||
const arr = (flags || []).filter(Boolean); | |||
if (!arr.length) return ''; | |||
const items = arr.map(k => `<img class="skill-flag" data-flag="${k}" alt="" src="${filePathURL(map[k])}">`).join(''); | |||
return `<div class="skill-flags" role="group" aria-label="Características">${items}</div>`; | |||
} | |||
} | } | ||
function | |||
function applyFlagTooltips(container) { | |||
if ( | const skillsRoot = document.getElementById('skills'); | ||
if (!skillsRoot) return; | |||
let pack = {}; | |||
try { pack = JSON.parse(skillsRoot.dataset.i18nFlags || '{}'); } catch (e) { } | |||
const raw = (document.documentElement.lang || 'pt').toLowerCase(); | |||
const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt'); | |||
const dict = pack[lang] || pack.pt || {}; | |||
const flags = container.querySelectorAll('.skill-flags .skill-flag[data-flag]'); | |||
const tooltip = window.__globalSkillTooltip; | const tooltip = window.__globalSkillTooltip; | ||
if (tooltip) { | if (!tooltip) return; | ||
const | flags.forEach(el => { | ||
if ( | const key = el.getAttribute('data-flag'); | ||
tooltip.show( | const tip = (dict && dict[key]) || ''; | ||
if (!tip) return; | |||
if (el.dataset.flagTipWired) return; | |||
el.dataset.flagTipWired = '1'; | |||
el.setAttribute('aria-label', tip); | |||
if (el.hasAttribute('title')) el.removeAttribute('title'); | |||
el.addEventListener('mouseenter', () => { | |||
const tipEl = document.querySelector('.skill-tooltip'); | |||
if (tipEl) tipEl.classList.add('flag-tooltip'); | |||
tooltip.show(el, tip); | |||
}); | |||
el.addEventListener('mousemove', () => { | |||
if (performance.now() >= tooltip.lockUntil.value) { | |||
tooltip.measureAndPos(el); | |||
} | |||
}); | |||
el.addEventListener('click', () => { | |||
tooltip.lockUntil.value = performance.now() + 240; | |||
tooltip.measureAndPos(el); | |||
}); | }); | ||
el.addEventListener('mouseleave', () => { | |||
const | const tipEl = document.querySelector('.skill-tooltip'); | ||
if ( | if (tipEl) tipEl.classList.remove('flag-tooltip'); | ||
tooltip.hide(); | tooltip.hide(); | ||
}); | }); | ||
} | }); | ||
} | } | ||
function ensureRail(iconsBar) { | |||
console.log('[DEBUG Subskills] ensureRail chamado, iconsBar:', iconsBar); | |||
const rail = iconsBar.closest('.top-rail'); | |||
if (!rail) { | |||
const | console.error('[DEBUG Subskills] ensureRail: .top-rail não encontrado! iconsBar:', iconsBar); | ||
return null; | |||
. | |||
} | } | ||
console.log('[DEBUG Subskills] ensureRail: rail encontrado:', rail); | |||
if ( | if (!subRail) { | ||
subRail = document.createElement('div'); | |||
subRail.className = 'subskills-rail collapsed hidden'; | |||
rail.appendChild(subRail); | |||
console.log('[DEBUG Subskills] ensureRail: subRail criado e anexado:', subRail); | |||
} else { | } else { | ||
console.log('[DEBUG Subskills] ensureRail: subRail já existe:', subRail); | |||
} | } | ||
if (!subBar) { | |||
subBar = document.createElement('div'); | |||
subBar.className = 'subicon-bar'; | |||
subRail.appendChild(subBar); | |||
} | console.log('[DEBUG Subskills] ensureRail: subBar criado e anexado:', subBar); | ||
} else { | |||
console.log('[DEBUG Subskills] ensureRail: subBar já existe:', subBar); | |||
} | } | ||
if (! | if (!spacer) { | ||
spacer = document.createElement('div'); | |||
spacer.className = 'subskills-spacer'; | |||
rail.parentNode.insertBefore(spacer, rail.nextSibling); | |||
console.log('[DEBUG Subskills] ensureRail: spacer criado e anexado:', spacer); | |||
} | } | ||
return rail; | |||
} | |||
if ( | function ensureSubVideoCached(s, parentIdx, videoBox) { | ||
const key = `sub:${parentIdx}:${(s.name || s.n || '').trim()}`; | |||
return | if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) { | ||
const precreated = window.__subskillVideosCache.get(key); | |||
if (!subCache.has(key)) { | |||
subCache.set(key, precreated); | |||
} | |||
return key; | |||
} | |||
if (subCache.has(key)) return key; | |||
if (!s.video || s.video.trim() === '') return key; | |||
const videoFileName = s.video.trim(); | |||
if (videoFileName === 'Nada.png' || videoFileName.toLowerCase().includes('nada.png')) { | |||
return key; | |||
} | } | ||
const v = document.createElement('video'); | const v = document.createElement('video'); | ||
v.className = 'skill-video'; | v.className = 'skill-video'; | ||
v.dataset.sub = '1'; | |||
v.setAttribute('controls', ''); | v.setAttribute('controls', ''); | ||
v.setAttribute('preload', ' | v.setAttribute('preload', 'auto'); | ||
v.setAttribute('playsinline', ''); | v.setAttribute('playsinline', ''); | ||
v.style | Object.assign(v.style, { display: 'none', width: '100%', height: 'auto', aspectRatio: '16/9', objectFit: 'cover' }); | ||
const videoURL = normalizeFileURL(videoFileName); | |||
if (!videoURL || videoURL.trim() === '') return key; | |||
// Detectar formato do vídeo pela extensão | // Detectar formato do vídeo pela extensão | ||
const ext = (videoURL.split('.').pop() || '').toLowerCase().split('?')[0]; | const ext = (videoURL.split('.').pop() || '').toLowerCase().split('?')[0]; | ||
| Linha 764: | Linha 405: | ||
v.setAttribute('webkit-playsinline', ''); | v.setAttribute('webkit-playsinline', ''); | ||
v.setAttribute('x-webkit-airplay', 'allow'); | v.setAttribute('x-webkit-airplay', 'allow'); | ||
videoBox.appendChild(v); | |||
subCache.set(key, v); | |||
if (window.__subskillVideosCache) { | |||
window.__subskillVideosCache.set(key, v); | |||
} | |||
v.load(); | |||
return key; | |||
} | |||
// Cria vídeo de weapon em cache (para subskills com weapon) - lógica do arquivo antigo | |||
function ensureSubVideoInCache(videoFileName, key, videoBox) { | |||
if (subCache.has(key)) return key; | |||
if (!videoFileName || videoFileName.trim() === '') return key; | |||
const videoFile = videoFileName.trim(); | |||
if (videoFile === 'Nada.png' || videoFile.toLowerCase().includes('nada.png')) { | |||
return key; | |||
return; | |||
} | } | ||
const videoURL = normalizeFileURL( | |||
if (!videoURL || videoURL.trim() === '') { | const v = document.createElement('video'); | ||
v.className = 'skill-video'; | |||
v.dataset.sub = '1'; | |||
v.dataset.weapon = '1'; | |||
v.setAttribute('controls', ''); | |||
v.setAttribute('preload', 'auto'); | |||
v.setAttribute('playsinline', ''); | |||
Object.assign(v.style, { display: 'none', width: '100%', height: 'auto', aspectRatio: '16/9', objectFit: 'cover' }); | |||
const videoURL = normalizeFileURL(videoFile); | |||
if (!videoURL || videoURL.trim() === '') return key; | |||
const ext = (videoURL.split('.').pop() || '').toLowerCase().split('?')[0]; | |||
const mimeTypes = { | |||
'mp4': 'video/mp4', | |||
'm4v': 'video/mp4', | |||
'webm': 'video/webm', | |||
'ogv': 'video/ogg', | |||
'ogg': 'video/ogg', | |||
'mov': 'video/quicktime' | |||
}; | |||
const mimeType = mimeTypes[ext] || 'video/mp4'; | |||
const src = document.createElement('source'); | |||
src.src = videoURL; | |||
src.type = mimeType; | |||
v.appendChild(src); | |||
v.setAttribute('webkit-playsinline', ''); | |||
v.setAttribute('x-webkit-airplay', 'allow'); | |||
videoBox.appendChild(v); | |||
subCache.set(key, v); | |||
if (window.__subskillVideosCache) { | |||
window.__subskillVideosCache.set(key, v); | |||
} | } | ||
v.load(); | |||
return key; | |||
} | |||
function showSubVideo(key, videoBox) { | |||
if (!videoBox) return; | |||
videoBox.querySelectorAll('.skill-video').forEach(v => { | |||
try { | try { | ||
v.pause(); | v.pause(); | ||
| Linha 897: | Linha 470: | ||
v.style.display = 'none'; | v.style.display = 'none'; | ||
}); | }); | ||
let v = null; | |||
if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) { | |||
v = window.__subskillVideosCache.get(key); | |||
} | } | ||
if (!v) { | |||
if ( | v = subCache.get(key); | ||
v = | |||
} | } | ||
if (!v) { | if (!v) { | ||
videoBox.style.display = 'none'; | |||
return; | |||
} | } | ||
videoBox.style.display = 'block'; | videoBox.style.display = 'block'; | ||
| Linha 961: | Linha 489: | ||
const suppress = document.body.dataset.suppressSkillPlay === '1'; | const suppress = document.body.dataset.suppressSkillPlay === '1'; | ||
if (!suppress) { | if (!suppress) { | ||
v.play().catch(() => { | v.play?.().catch(() => { | ||
}); | }); | ||
} else { | } | ||
} | |||
api.refreshCurrentSubSafe = function () { | |||
const btn = document.querySelector('.subskills-rail .subicon.active'); | |||
if (!btn) return false; | |||
const had = document.body.dataset.suppressSkillPlay; | |||
document.body.dataset.suppressSkillPlay = '1'; | |||
try { | |||
btn.dispatchEvent(new Event('click', { bubbles: true })); | |||
} finally { | |||
if (had) document.body.dataset.suppressSkillPlay = had; | |||
else delete document.body.dataset.suppressSkillPlay; | |||
} | |||
return true; | |||
}; | |||
// Função auxiliar para aplicar classes de weapon nas subskills renderizadas | |||
// PADRONIZADO: usa data-weapon (igual a Character.Skills.html) | |||
const applyWeaponClassesToSubskills = () => { | |||
const weaponOn = isWeaponModeOn(); | |||
// Busca TODAS as subskills com weapon usando o seletor correto | |||
// Tenta múltiplos seletores para garantir que encontra | |||
let weaponSubs = document.querySelectorAll('.subicon[data-weapon]'); | |||
if (weaponSubs.length === 0) { | |||
// Tenta buscar em todas as subbars | |||
const allSubbars = document.querySelectorAll('.subicon-bar'); | |||
if (allSubbars.length > 0) { | |||
console.log('[Subskills] Nenhuma subskill encontrada com .subicon[data-weapon], tentando buscar em todas as subbars:', allSubbars.length); | |||
allSubbars.forEach(bar => { | |||
const subsInBar = bar.querySelectorAll('.subicon[data-weapon]'); | |||
if (subsInBar.length > 0) { | |||
console.log('[Subskills] Subbar encontrada com', subsInBar.length, 'subskills com weapon'); | |||
weaponSubs = Array.from(weaponSubs).concat(Array.from(subsInBar)); | |||
} | |||
}); | |||
} | } | ||
} | } | ||
// Remove duplicatas | |||
weaponSubs = Array.from(new Set(Array.from(weaponSubs))); | |||
if (weaponSubs.length > 0) { | |||
console.log('[Subskills] applyWeaponClassesToSubskills chamado, weaponOn:', weaponOn, 'subskills com weapon:', weaponSubs.length); | |||
if ( | } | ||
// Só faz log detalhado se realmente encontrou subskills | |||
if (weaponSubs.length === 0 && document.querySelectorAll('.subicon').length > 0) { | |||
// Log adicional para debug apenas se há subicons mas nenhum com weapon | |||
const allSubicons = document.querySelectorAll('.subicon'); | |||
console.log('[Subskills] Total de subicons no DOM:', allSubicons.length); | |||
allSubicons.forEach((icon, idx) => { | |||
const hasData = icon.hasAttribute('data-weapon'); | |||
const title = icon.title || icon.dataset.slug || `subicon-${idx}`; | |||
console.log(`[Subskills] Subicon ${idx} "${title}": data-weapon=${hasData}`); | |||
}); | |||
} | |||
weaponSubs.forEach(el => { | |||
if (weaponOn) { | |||
el.classList.add('has-weapon-available'); | |||
} | if (el.classList.contains('active')) { | ||
el.classList.add('weapon-equipped'); | |||
// Só log se há subskill ativa | |||
console.log('[Subskills] Subskill ativa marcada como weapon-equipped:', el.title || el.dataset.slug); | |||
} | |||
} else { | } else { | ||
el.classList.remove('has-weapon-available'); | |||
el.classList.remove('weapon-equipped'); | |||
} | } | ||
}); | |||
}; | |||
api.renderBarFrom = function (el, { iconsBar, descBox, videoBox }) { | |||
console.log('[DEBUG Subskills] renderBarFrom chamado, iconsBar:', iconsBar); | |||
const rail = ensureRail(iconsBar); | |||
if (!rail) { | |||
console.error('[DEBUG Subskills] ensureRail retornou null! iconsBar:', iconsBar); | |||
return; | |||
} | } | ||
console.log('[DEBUG Subskills] rail encontrado:', rail, 'subRail:', subRail, 'subBar:', subBar); | |||
const rawSubs = el.getAttribute('data-subs') || ''; | |||
const rawOrder = el.getAttribute('data-suborder') || ''; | |||
const parentIdx = el.dataset.index || ''; | |||
if (!rawSubs.trim()) { | |||
console.log('[DEBUG Subskills] Sem data-subs, ocultando subskills'); | |||
if (subRail) subRail.classList.add('collapsed'); | |||
if (subRail) subRail.classList.add('hidden'); | |||
if (subBar) subBar.innerHTML = ''; | |||
if (spacer) spacer.style.height = '0px'; | |||
if ( | |||
if ( | |||
if ( | |||
return; | return; | ||
} | } | ||
let subs; | |||
try { subs = JSON.parse(rawSubs); } catch { subs = []; } | |||
// DEBUG: Verifica se weapon está presente no JSON do Lua | |||
console.log('[Subskills] Total de subskills recebidas:', subs.length); | |||
console.log('[Subskills] JSON completo recebido do Lua:', JSON.stringify(subs, null, 2)); | |||
subs.forEach((sub, idx) => { | |||
const subName = (sub.name || sub.n || '').trim(); | |||
if (subName === 'Karmic Jishin') { | |||
console.log('[Subskills] Karmic Jishin do JSON Lua:', { | |||
index: idx, | |||
name: subName, | |||
weapon: sub.weapon, | |||
weaponType: typeof sub.weapon, | |||
weaponKeys: sub.weapon ? Object.keys(sub.weapon) : null, | |||
hasDesc_i18n: !!(sub.weapon && sub.weapon.desc_i18n), | |||
hasDesc: !!(sub.weapon && sub.weapon.desc), | |||
fullSub: JSON.stringify(sub, null, 2) | |||
}); | |||
} | } | ||
// Log todas as subskills com weapon | |||
if (sub.weapon) { | |||
console.log(`[Subskills] Subskill "${subName}" TEM WEAPON:`, { | |||
weapon: sub.weapon, | |||
keys: Object.keys(sub.weapon), | |||
if ( | hasDesc_i18n: !!sub.weapon.desc_i18n, | ||
hasDesc: !!sub.weapon.desc | |||
}); | }); | ||
}); | } else { | ||
console.log(`[Subskills] Subskill "${subName}" SEM WEAPON`); | |||
} | |||
}); | }); | ||
// NORMALIZADOR: Converte weaponPacked para weapon se necessário (fallback para dados legados) | |||
subs = subs.map(sub => { | |||
// Se tem weaponPacked mas não tem weapon, tenta parsear | |||
if (sub.weaponPacked && !sub.weapon && typeof sub.weaponPacked === 'string' && sub.weaponPacked.trim() !== '') { | |||
const parts = sub.weaponPacked.split('~'); | |||
if (parts.length >= 2) { | |||
sub.weapon = { | |||
icon: parts[0] || 'Nada.png', | |||
powerpve: parts[1] || null, | |||
powerpvp: parts[2] || null, | |||
cooldown: parts[3] || null, | |||
video: parts[4] || '', | |||
energy: parts[5] || null | |||
}; | |||
// Remove valores vazios | |||
Object.keys(sub.weapon).forEach(k => { | |||
if (sub.weapon[k] === '' || sub.weapon[k] === null) { | |||
delete sub.weapon[k]; | |||
} | |||
}); | |||
} | } | ||
} | } | ||
if ( | // Garante que weapon seja objeto válido | ||
if (sub.weapon && typeof sub.weapon === 'string') { | |||
// Tenta parsear como JSON primeiro | |||
try { | try { | ||
weapon = JSON.parse( | sub.weapon = JSON.parse(sub.weapon); | ||
} catch ( | } catch { | ||
// Se falhar, tenta formato ~ | |||
const parts = sub.weapon.split('~'); | |||
if (parts.length >= 2) { | |||
sub.weapon = { | |||
icon: parts[0] || 'Nada.png', | |||
powerpve: parts[1] || null, | |||
powerpvp: parts[2] || null, | |||
cooldown: parts[3] || null, | |||
video: parts[4] || '', | |||
energy: parts[5] || null | |||
}; | |||
Object.keys(sub.weapon).forEach(k => { | |||
if (sub.weapon[k] === '' || sub.weapon[k] === null) { | |||
delete sub.weapon[k]; | |||
} | |||
}); | |||
} else { | |||
sub.weapon = null; | |||
} | |||
} | } | ||
} | } | ||
return sub; | |||
}); | }); | ||
if (!Array.isArray(subs) || subs.length === 0) { | |||
if (! | subRail.classList.add('collapsed'); | ||
subRail.classList.add('hidden'); | |||
subBar.innerHTML = ''; | |||
if (spacer) spacer.style.height = '0px'; | |||
return; | |||
} | |||
// Busca mapa das skills principais para herança | |||
const mainSkills = getMainSkillsMap(); | |||
// Aplica herança ANTES de processar - isso resolve nome, icon, etc. | |||
subs = subs.map(sub => applyInheritance(sub, mainSkills)); | |||
// Verifica weapon nas subskills | |||
subs.forEach((s, i) => { | |||
if (s.weapon) { | |||
console.log(`[Subskills] Sub ${i} TEM WEAPON:`, JSON.stringify(s.weapon)); | |||
} | |||
} | |||
const | |||
if ( | |||
}); | }); | ||
// Remove subskills que ficaram sem nome após herança (herança falhou) | |||
subs = subs.filter(s => (s.name || s.n || '').trim() !== ''); | |||
subRail.classList.add('hidden'); | |||
subBar.innerHTML = ''; | |||
if ( | |||
// Usa a ordem natural das subskills após herança | |||
let order = subs.map(s => s.name || s.n || ''); | |||
if (rawOrder.trim()) { | |||
try { | |||
const preferred = JSON.parse(rawOrder); | |||
if (Array.isArray(preferred) && preferred.length) { | |||
const byName = new Map(subs.map(s => [(s.name || s.n || ''), s])); | |||
order = preferred.filter(n => byName.has(n)); | |||
} | |||
} catch { } | |||
} | |||
order.forEach(nm => { | |||
const s = subs.find(x => (x.name || x.n || '') === nm); | |||
if (s) { | |||
if (s.video) ensureSubVideoCached(s, parentIdx, videoBox); | |||
if (s.icon) preloadImage(s.icon); | |||
} | |||
}); | }); | ||
// DEBUG: Log todas as subskills ANTES de renderizar | |||
console.log('[Subskills] Renderizando', subs.length, 'subskills. Verificando weapon...'); | |||
subs.forEach((sub, idx) => { | |||
const subName = (sub.name || sub.n || '').trim(); | |||
if ( | if (sub.weapon) { | ||
console.log(`[Subskills] Subskill ${idx} "${subName}" TEM WEAPON antes de renderizar:`, { | |||
weapon: sub.weapon, | |||
weaponType: typeof sub.weapon, | |||
weaponKeys: typeof sub.weapon === 'object' ? Object.keys(sub.weapon) : null | |||
}); | |||
} | } else { | ||
if (subName === 'Karmic Jishin') { | |||
console.warn(`[Subskills] Subskill ${idx} "${subName}" SEM WEAPON antes de renderizar!`, sub); | |||
} | |||
} | } | ||
} | }); | ||
console.log('[DEBUG Subskills] Iniciando criação de subicons, order.length:', order.length, 'subs.length:', subs.length); | |||
order.forEach(nm => { | |||
const s = subs.find(x => (x.name || x.n || '') === nm); | |||
if (!s) { | |||
console.warn('[DEBUG Subskills] Subskill não encontrada para nome:', nm); | |||
return; | return; | ||
} | } | ||
console.log('[DEBUG Subskills] Criando subicon:', s.name || s.n || nm); | |||
const item = document.createElement('div'); | |||
item.className = 'subicon'; | |||
item.title = s.name || nm; | |||
const slugify = window.__skillSlugify || ((str) => (str || '').toLowerCase().replace(/[^\w]+/g, '-').replace(/^-+|-+$/g, '')); | |||
item.dataset.slug = slugify(s.name || nm); | |||
const img = document.createElement('img'); | |||
img.alt = ''; | |||
img.src = filePathURL(s.icon || 'Nada.png'); | |||
item.appendChild(img); | |||
const | |||
// PADRONIZADO: usa data-weapon (igual a Character.Skills.html) | |||
// Verifica weapon de forma mais robusta | |||
const hasWeapon = s.weapon && ( | |||
(typeof s.weapon === 'object' && Object.keys(s.weapon).length > 0) || | |||
(typeof s.weapon === 'string' && s.weapon.trim() !== '') | |||
); | |||
const subName = (s.name || s.n || '').trim(); | |||
if (hasWeapon) { | |||
// Normaliza weapon se for string | |||
let weaponObj = s.weapon; | |||
if (typeof weaponObj === 'string') { | |||
try { | |||
weaponObj = JSON.parse(weaponObj); | |||
} catch { | |||
// Se falhar, tenta formato ~ | |||
const parts = weaponObj.split('~'); | |||
if (parts.length >= 2) { | |||
weaponObj = { | |||
icon: parts[0] || 'Nada.png', | |||
powerpve: parts[1] || null, | |||
powerpvp: parts[2] || null, | |||
cooldown: parts[3] || null, | |||
video: parts[4] || '', | |||
energy: parts[5] || null | |||
}; | |||
Object.keys(weaponObj).forEach(k => { | |||
if (weaponObj[k] === '' || weaponObj[k] === null) { | |||
delete weaponObj[k]; | |||
} | } | ||
console. | }); | ||
} else { | |||
weaponObj = null; | |||
} | |||
} | |||
} | |||
if (weaponObj && typeof weaponObj === 'object' && Object.keys(weaponObj).length > 0) { | |||
try { | |||
item.dataset.weapon = JSON.stringify(weaponObj); | |||
console.log(`[Subskills] "${subName}" - data-weapon definido, weapon:`, weaponObj); | |||
} catch (e) { | |||
console.error('[Subskills] Erro ao serializar weapon de subskill', subName, e); | |||
} | |||
// Aplica classe inicial se o toggle já está ativo | |||
if (isWeaponModeOn()) { | |||
item.classList.add('has-weapon-available'); | |||
} | |||
} else { | |||
console.warn(`[Subskills] "${subName}" - weapon inválido após normalização:`, weaponObj); | |||
} | |||
} else { | |||
if (subName === 'Karmic Jishin') { | |||
console.warn('[Subskills] Karmic Jishin - SEM WEAPON!', { | |||
s: s, | |||
weapon: s.weapon, | |||
weaponType: typeof s.weapon, | |||
weaponKeys: s.weapon ? Object.keys(s.weapon) : null | |||
}); | |||
} | } | ||
} | } | ||
item.addEventListener('click', () => { | |||
const L = getLabels(); | |||
const subName = (s.name || s.n || '').trim(); | |||
// PADRONIZADO: lê weapon diretamente do atributo data-weapon (igual a Character.Skills.html) | |||
let subWeaponData = null; | |||
if (item.dataset.weapon) { | |||
try { | |||
subWeaponData = JSON.parse(item.dataset.weapon); | |||
} catch (e) { | |||
console.warn('[Subskills] Erro ao parsear data-weapon:', e); | |||
subWeaponData = null; | |||
} | |||
} | |||
const hasSubWeapon = !!subWeaponData; | |||
const weaponOn = isWeaponModeOn(); | |||
const weaponEquipped = weaponOn && hasSubWeapon && subWeaponData; | |||
console.log('[Subskills] click handler debug:', { | |||
subName, | |||
weaponOn, | |||
hasSubWeapon, | |||
weaponEquipped, | |||
subWeaponData: subWeaponData | |||
}); | |||
// DEBUG: Log para Karmic Jishin | |||
if (subName === 'Karmic Jishin') { | |||
console.log('[Subskills Click] Karmic Jishin:', { | |||
weaponOn, | |||
hasSubWeapon, | |||
weaponEquipped, | |||
subWeaponData: subWeaponData | |||
}); | |||
} | |||
// Determina descrição (weapon ou normal) | |||
const raw = (document.documentElement.lang || 'pt').toLowerCase(); | |||
const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt'); | |||
let chosen = ''; | |||
if (weaponEquipped && subWeaponData) { | |||
// Descrição do weapon | |||
const weaponDescPack = subWeaponData.desc_i18n || subWeaponData.desc || {}; | |||
chosen = weaponDescPack[lang] || weaponDescPack.pt || weaponDescPack.en || ''; | |||
} else { | |||
// Descrição normal | |||
const base = s.desc_i18n || s.desc || {}; | |||
chosen = base[lang] || base.pt || base.en || (s.descPt || ''); | |||
} | |||
// Determina atributos (weapon ou normal) | |||
let attrsObj = { | |||
powerpve: s.powerpve, | |||
powerpvp: s.powerpvp, | |||
energy: s.energy, | |||
cooldown: s.cooldown | |||
}; | }; | ||
if (weaponEquipped && subWeaponData) { | |||
attrsObj = { | |||
powerpve: subWeaponData.powerpve || s.powerpve, | |||
powerpvp: subWeaponData.powerpvp || s.powerpvp, | |||
energy: subWeaponData.energy || s.energy, | |||
cooldown: subWeaponData.cooldown || s.cooldown | |||
}; | |||
} | |||
// Level (weapon ou normal) | |||
const level = (weaponEquipped && subWeaponData && subWeaponData.level) | |||
? subWeaponData.level.toString().trim() | |||
: (s.level || '').toString().trim(); | |||
let flagsHTML = ''; | |||
if (Array.isArray(s.flags) && s.flags.length > 0) { | |||
flagsHTML = renderFlagsRow(s.flags); | |||
} | |||
if (descBox) { | |||
descBox.innerHTML = `<div class="skill-title"><h3>${s.name || nm}</h3></div>${level ? `<div class="skill-level-line"><span class="attr-label">${L.level} ${level}</span></div>` : ''}${renderSubAttrs(attrsObj, L)}<div class="desc">${chosen.replace(/'''(.*?)'''/g, '<b>$1</b>')}</div>`; | |||
} | |||
if (videoBox) { | |||
const oldFlags = videoBox.querySelector('.skill-flags'); | |||
if (oldFlags) oldFlags.remove(); | |||
if (flagsHTML) { | |||
videoBox.insertAdjacentHTML('beforeend', flagsHTML); | |||
applyFlagTooltips(videoBox); | |||
} | |||
} | |||
// Vídeo (weapon ou normal) - usa valores explícitos do módulo Lua | |||
const effectiveVideo = getEffectiveVideo(s); | |||
console.log('[Subskills] effectiveVideo calculado:', { | |||
subName, | |||
weaponOn: weaponEquipped, | |||
hasWeaponData: !!subWeaponData, | |||
weaponVideo: subWeaponData?.video, | |||
baseVideo: s.video, | |||
effectiveVideo | |||
}); | |||
if (!effectiveVideo || effectiveVideo.trim() === '' || effectiveVideo === 'Nada.png' || effectiveVideo.toLowerCase().includes('nada.png')) { | |||
if (videoBox) videoBox.style.display = 'none'; | |||
console.log('[Subskills] video escolhido (nenhum)', parentIdx, subName, weaponOn, 'sem vídeo'); | |||
} else { | |||
const videoKey = (weaponEquipped && subWeaponData && subWeaponData.video && subWeaponData.video.trim() !== '') | |||
? `sub:${parentIdx}:${subName}:weapon` | |||
: `sub:${parentIdx}:${subName}`; | |||
console.log('[Subskills] video escolhido', parentIdx, subName, weaponOn, effectiveVideo); | |||
// Se tem weapon video e está ativo, precisa criar/mostrar esse vídeo | |||
if (weaponEquipped && subWeaponData && subWeaponData.video && subWeaponData.video.trim() !== '' && subWeaponData.video !== 'Nada.png') { | |||
ensureSubVideoInCache(subWeaponData.video, videoKey, videoBox); | |||
} else if (s.video && s.video.trim() !== '' && s.video !== 'Nada.png') { | |||
ensureSubVideoCached(s, parentIdx, videoBox); | |||
} | |||
showSubVideo(videoKey, videoBox); | |||
} | |||
Array.from(subBar.children).forEach(c => { | |||
c.classList.remove('active'); | |||
c.classList.remove('weapon-equipped'); | |||
}); | |||
item.classList.add('active'); | |||
// Aplica weapon-equipped se tem weapon e está ativo (SISTEMA SEPARADO) | |||
if (weaponEquipped) { | |||
item.classList.add('weapon-equipped'); | |||
} | |||
window.__lastActiveSkillIcon = item; | |||
}); | }); | ||
if (window.__globalSkillTooltip) { | |||
const { show, hide, measureAndPos, lockUntil } = window.__globalSkillTooltip; | |||
const label = item.title || ''; | |||
item.setAttribute('aria-label', label); | |||
if (item.hasAttribute('title')) item.removeAttribute('title'); | |||
item.addEventListener('mouseenter', () => show(item, label)); | |||
item.addEventListener('mousemove', () => { if (performance.now() >= lockUntil.value) measureAndPos(item); }); | |||
item.addEventListener('click', () => { lockUntil.value = performance.now() + 240; measureAndPos(item); }); | |||
item.addEventListener('mouseleave', hide); | |||
} | |||
console.log('[DEBUG Subskills] Anexando subicon ao subBar:', item.title, 'subBar:', subBar); | |||
subBar.appendChild(item); | |||
}); | }); | ||
// DEBUG: Verifica quantos subicons foram criados | |||
const totalSubicons = subBar.querySelectorAll('.subicon').length; | |||
if ( | console.log('[DEBUG Subskills] Total de subicons criados e anexados:', totalSubicons); | ||
if (totalSubicons === 0) { | |||
console.error('[DEBUG Subskills] ERRO: Nenhum subicon foi criado! subBar:', subBar, 'subBar.innerHTML:', subBar.innerHTML); | |||
} | } | ||
// Aplica classes de weapon nas subskills recém-renderizadas se o toggle estiver ativo | |||
// Aplica classes de weapon DEPOIS que todos os items foram adicionados ao DOM | |||
setTimeout(() => { | |||
const finalCount = document.querySelectorAll('.subicon').length; | |||
console.log('[DEBUG Subskills] Total de subicons no DOM após timeout:', finalCount); | |||
applyWeaponClassesToSubskills(); | |||
// Dispara evento para notificar que subskills estão prontas | |||
window.dispatchEvent(new CustomEvent('gla:subskills:ready', { detail: { count: finalCount } })); | |||
// REGISTRA LISTENER DO EVENTO DENTRO DO renderBarFrom (após renderizar) | |||
// Remove listener anterior se existir (evita duplicação) | |||
if (subBar._weaponToggleListener) { | |||
window.removeEventListener('gla:weaponToggled', subBar._weaponToggleListener); | |||
if ( | |||
} | } | ||
// Cria novo listener que opera no subBar atual | |||
subBar._weaponToggleListener = (e) => { | |||
const enabled = e.detail?.enabled ?? false; | |||
console.log('[Subskills] Evento gla:weaponToggled recebido (dentro de renderBarFrom):', enabled); | |||
const | |||
// Aplica classes usando a função auxiliar | |||
applyWeaponClassesToSubskills(); | |||
// Atualiza a subskill ativa se houver (recarrega descrição/atributos) | |||
setTimeout(() => { | |||
const activeSub = subBar.querySelector('.subicon[data-weapon].active') | |||
|| subBar.querySelector('.subicon.active'); | |||
console.log('[Subskills] Atualizando subskill ativa:', activeSub ? (activeSub.title || activeSub.dataset.slug) : 'nenhuma'); | |||
if (activeSub) { | |||
activeSub.dispatchEvent(new Event('click', { bubbles: true })); | |||
if ( | |||
} else { | } else { | ||
api.refreshCurrentSubSafe(); | |||
} | } | ||
} | }, 50); | ||
} | }; | ||
window.addEventListener('gla:weaponToggled', subBar._weaponToggleListener); | |||
requestAnimationFrame(() => { | requestAnimationFrame(() => { | ||
subRail.classList.remove('collapsed'); | |||
subRail.classList.remove('hidden'); | |||
const h = subRail.offsetHeight || 48; | |||
if (spacer) spacer.style.height = h + 'px'; | |||
}); | }); | ||
}, 10); | |||
}; | |||
api.hideAll = function (videoBox) { | |||
videoBox?.querySelectorAll('.skill-video[data-sub="1"]').forEach(v => { | |||
try { v.pause(); } catch { } | |||
v.style.display = 'none'; | |||
}); | |||
}; | |||
window.renderSubskillsBarFrom = function (el, ctx) { api.renderBarFrom(el, ctx); }; | |||
api.preloadAllSubskillImages = function () { | |||
const allSkillIcons = document.querySelectorAll('.icon-bar .skill-icon[data-subs]'); | |||
const | const preloadPromises = []; | ||
if ( | let totalImages = 0; | ||
if ( | allSkillIcons.forEach(icon => { | ||
try { | |||
const subsRaw = icon.getAttribute('data-subs'); | |||
if (!subsRaw) return; | |||
const subs = JSON.parse(subsRaw); | |||
if (!Array.isArray(subs)) return; | |||
subs.forEach(s => { | |||
if (s && s.icon) { | |||
} | preloadPromises.push(preloadImage(s.icon)); | ||
totalImages++; | |||
} | |||
if (s && Array.isArray(s.subs)) { | |||
s.subs.forEach(nested => { | |||
if (nested && nested.icon) { | |||
preloadPromises.push(preloadImage(nested.icon)); | |||
totalImages++; | |||
if ( | |||
} | } | ||
}); | }); | ||
} | } | ||
}, | }); | ||
} catch (e) { | |||
} | |||
}); | |||
if (totalImages > 0) { | |||
return Promise.all(preloadPromises).then(() => { | |||
}); | |||
} | |||
return Promise.resolve(); | |||
}; | |||
// Inicialização: constrói cache das skills principais e pré-carrega imagens | |||
function init() { | |||
// Constrói cache das skills principais ANTES de qualquer interação | |||
getMainSkillsMap(); | |||
// Pré-carrega imagens das subskills | |||
api.preloadAllSubskillImages(); | |||
// Escuta mudanças no localStorage para atualizar subskill ativa | |||
window.addEventListener('storage', (e) => { | |||
if (e.key === 'glaWeaponEnabled') { | |||
setTimeout(() => api.refreshCurrentSubSafe(), 50); | |||
} | |||
}); | |||
// LISTENER GLOBAL: Escuta evento de toggle e aplica em todas as subskills | |||
// Este listener SEMPRE funciona, mesmo se as subskills ainda não foram renderizadas | |||
window.addEventListener('gla:weaponToggled', (e) => { | |||
const enabled = e.detail?.enabled ?? false; | |||
console.log('[Subskills] Evento gla:weaponToggled recebido (global):', enabled); | |||
// Tenta aplicar classes imediatamente | |||
applyWeaponClassesToSubskills(); | |||
// Se não encontrou subskills, tenta novamente após um delay (caso ainda estejam sendo renderizadas) | |||
const weaponSubs = document.querySelectorAll('.subicon[data-subweapon]'); | |||
if (weaponSubs.length === 0) { | |||
console.log('[Subskills] Nenhuma subskill encontrada, tentando novamente após delay...'); | |||
setTimeout(() => { | |||
applyWeaponClassesToSubskills(); | |||
const activeSub = document.querySelector('.subicon[data-subweapon].active') | |||
|| document.querySelector('.subicon.active'); | |||
if (activeSub) { | |||
activeSub.dispatchEvent(new Event('click', { bubbles: true })); | |||
} | |||
}, 200); | |||
} else { | |||
// Atualiza a subskill ativa se houver | |||
setTimeout(() => { | setTimeout(() => { | ||
const activeSub = document.querySelector('.subicon[data-weapon].active') | |||
|| document.querySelector('.subicon.active'); | |||
console.log('[Subskills] Atualizando subskill ativa:', activeSub ? (activeSub.title || activeSub.dataset.slug) : 'nenhuma'); | |||
if (activeSub) { | |||
activeSub.dispatchEvent(new Event('click', { bubbles: true })); | |||
} else { | } else { | ||
api.refreshCurrentSubSafe(); | |||
} | } | ||
}, | }, 50); | ||
} | } | ||
}); | }); | ||
// LISTENER: Escuta quando subskills estão prontas e aplica classes de weapon | |||
window.addEventListener('gla:subskills:ready', (e) => { | |||
console.log('[Subskills] Evento gla:subskills:ready recebido, count:', e.detail?.count); | |||
// Aplica classes de weapon se o toggle estiver ativo | |||
applyWeaponClassesToSubskills(); | |||
}); | }); | ||
} | } | ||
if ( | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', () => { | |||
setTimeout(init, 100); | |||
}); | }); | ||
} else { | |||
setTimeout(init, 100); | |||
} | |||
} | |||
})(); | })(); | ||
</script> | </script> | ||
<style> | |||
.subicon-bar { | |||
display: flex; | |||
gap: 10px; | |||
padding: 6px 6px; | |||
overflow-x: auto; | |||
/* Firefox */ | |||
scrollbar-width: thin; | |||
scrollbar-color: #ababab transparent; | |||
} | |||
.subicon-bar::-webkit-scrollbar { | |||
height: 6px; | |||
} | |||
.subicon-bar::-webkit-scrollbar-thumb { | |||
background: #151515; | |||
border-radius: 3px; | |||
} | |||
.subicon { | |||
width: var(--icon-size, 42px); | |||
height: var(--icon-size, 42px); | |||
border-radius: var(--icon-radius, 10px); | |||
overflow: hidden; | |||
position: relative; | |||
flex: 0 0 auto; | |||
cursor: pointer; | |||
isolation: isolate; | |||
} | |||
.subicon img { | |||
width: 100%; | |||
height: 100%; | |||
object-fit: cover; | |||
display: block; | |||
border-radius: inherit; | |||
} | |||
.subicon::after { | |||
content: ""; | |||
position: absolute; | |||
inset: 0; | |||
border-radius: inherit; | |||
box-shadow: inset 0 0 0 var(--icon-ring-w, 2px) var(--icon-idle, #cfcfcf); | |||
pointer-events: none; | |||
z-index: 2; | |||
transition: box-shadow .12s ease; | |||
} | |||
.subicon:hover::after { | |||
box-shadow: inset 0 0 0 var(--icon-ring-w, 2px) #e6e6e6; | |||
} | |||
.subicon.active::after { | |||
box-shadow: inset 0 0 0 var(--icon-ring-w, 2px) var(--icon-active, #FFD95A); | |||
} | |||
.subicon.active::before { | |||
content: ""; | |||
position: absolute; | |||
inset: -4px; | |||
border-radius: calc(var(--icon-radius, 10px) + 4px); | |||
pointer-events: none; | |||
z-index: 1; | |||
opacity: 1; | |||
box-shadow: 0 0 12px 3px var(--icon-active-glow, rgba(255, 217, 90, .30)), | |||
0 0 0 calc(var(--icon-ring-w, 2px) * 2) var(--icon-active-ring, rgba(255, 217, 90, .50)); | |||
} | |||
.top-rail.skills { | |||
position: relative; | |||
display: flex; | |||
flex-direction: column; | |||
align-items: center; | |||
overflow: visible; | |||
} | |||
.top-rail.skills .icon-bar { | |||
margin-bottom: 0; | |||
position: relative; | |||
z-index: 2; | |||
} | |||
.subskills-rail { | |||
position: absolute; | |||
left: 50%; | |||
transform: translateX(-50%); | |||
top: calc(100% - 1px); | |||
z-index: 3; | |||
display: inline-flex; | |||
justify-content: center; | |||
align-items: center; | |||
width: auto; | |||
max-width: 100%; | |||
padding: 3px 5px; | |||
background: rgba(0, 0, 0, .38); | |||
border: 1px solid rgba(255, 255, 255, .10); | |||
border-top: 1px solid rgba(255, 255, 255, .08); | |||
border-radius: 0 0 10px 10px; | |||
box-shadow: 0 3px 9px rgba(0, 0, 0, .22); | |||
-webkit-backdrop-filter: blur(2px); | |||
backdrop-filter: blur(2px); | |||
overflow: hidden; | |||
transition: opacity .14s ease, transform .14s ease; | |||
opacity: 1; | |||
} | |||
.subskills-rail::before { | |||
content: ""; | |||
position: absolute; | |||
top: -6px; | |||
left: 0; | |||
right: 0; | |||
height: 6px; | |||
background: linear-gradient(to bottom, rgba(0, 0, 0, .20), rgba(0, 0, 0, 0)); | |||
pointer-events: none; | |||
} | |||
.subskills-rail.collapsed { | |||
opacity: 0; | |||
pointer-events: none; | |||
transform: translate(-50%, -6px); | |||
} | |||
.subskills-rail.hidden { | |||
visibility: hidden; | |||
} | |||
.subskills-spacer { | |||
height: 0; | |||
transition: height .2s ease; | |||
} | |||
.subskills-rail .subicon-bar { | |||
display: inline-flex; | |||
align-items: center; | |||
gap: 0; | |||
overflow-x: auto; | |||
/* Firefox */ | |||
scrollbar-width: thin; | |||
scrollbar-color: #ababab transparent; | |||
} | |||
.subskills-rail .subicon-bar::-webkit-scrollbar { | |||
height: 6px; | |||
} | |||
.subskills-rail .subicon-bar::-webkit-scrollbar-thumb { | |||
background: #151515; | |||
border-radius: 3px; | |||
} | |||
.subskills-rail .subicon { | |||
width: 42px; | |||
height: 42px; | |||
border-radius: 6px; | |||
position: relative; | |||
overflow: hidden; | |||
flex: 0 0 auto; | |||
cursor: pointer; | |||
isolation: isolate; | |||
-webkit-backface-visibility: hidden; | |||
backface-visibility: hidden; | |||
transform: translateZ(0); | |||
} | |||
.subskills-rail .subicon+.subicon { | |||
margin-left: 4px; | |||
} | |||
.subskills-rail .subicon img { | |||
width: 100%; | |||
height: 100%; | |||
object-fit: cover; | |||
display: block; | |||
border-radius: inherit; | |||
} | |||
.subskills-rail .subicon::after { | |||
content: ""; | |||
position: absolute; | |||
inset: 0; | |||
border-radius: inherit; | |||
box-shadow: inset 0 0 0 2px var(--icon-idle, #cfcfcf); | |||
pointer-events: none; | |||
z-index: 2; | |||
transition: box-shadow .12s ease; | |||
} | |||
.subskills-rail .subicon:hover::after { | |||
box-shadow: inset 0 0 0 2px #e6e6e6; | |||
} | |||
.subskills-rail .subicon.active::after { | |||
box-shadow: inset 0 0 0 2px var(--icon-active, #FFD95A); | |||
} | |||
.video-container .skill-video { | |||
width: 100%; | |||
height: auto; | |||
aspect-ratio: 16 / 9; | |||
object-fit: cover; | |||
background: #000; | |||
border-radius: 10px; | |||
} | |||
@media (max-width: 900px) { | |||
.subskills-rail { | |||
position: static; | |||
transform: none; | |||
margin-top: -2px; | |||
border-top: 0; | |||
border-radius: 0 0 10px 10px; | |||
} | |||
.subskills-spacer { | |||
height: 0 !important; | |||
} | |||
} | |||
.skills-rail-wrap { | |||
position: relative; | |||
display: block; | |||
width: max-content; | |||
margin: 0 auto; | |||
} | |||
/* Subskills com arma disponível - borda vermelha quando inativa */ | |||
.subicon.has-weapon-available:not(.active)::after { | |||
box-shadow: inset 0 0 0 2px rgba(220, 70, 70, 0.8) !important; | |||
} | |||
/* Subskill com arma ATIVA - laranja/coral vibrante */ | |||
.subicon.has-weapon-available.active::after { | |||
box-shadow: inset 0 0 0 2px #FF7043 !important; | |||
} | |||
.subicon.has-weapon-available.active::before { | |||
box-shadow: 0 0 12px 3px rgba(255, 112, 67, 0.35), 0 0 0 4px rgba(255, 112, 67, 0.25) !important; | |||
} | |||
/* Modo arma ON - efeito animado nos subicons */ | |||
.top-rail.skills.weapon-mode-on .subicon.has-weapon-available { | |||
position: relative; | |||
} | |||
.top-rail.skills.weapon-mode-on .subicon.has-weapon-available::after { | |||
box-shadow: none !important; | |||
background: linear-gradient(90deg, | |||
rgba(255, 80, 80, 0.9) 0%, | |||
rgba(255, 120, 60, 1) 25%, | |||
rgba(255, 80, 80, 0.9) 50%, | |||
rgba(255, 120, 60, 1) 75%, | |||
rgba(255, 80, 80, 0.9) 100%) !important; | |||
background-size: 400% 100% !important; | |||
animation: weapon-subicon-border-scan 4s linear infinite !important; | |||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0) !important; | |||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0) !important; | |||
-webkit-mask-composite: xor !important; | |||
mask-composite: exclude !important; | |||
padding: 2px !important; | |||
} | |||
.top-rail.skills.weapon-mode-on .subicon.has-weapon-available::before { | |||
content: ""; | |||
position: absolute; | |||
inset: -4px; | |||
border-radius: calc(6px + 4px); | |||
pointer-events: none; | |||
z-index: 1; | |||
animation: weapon-subicon-pulse 3s ease-in-out infinite !important; | |||
} | |||
/* Subskill ativa com arma - mais intenso */ | |||
.top-rail.skills.weapon-mode-on .subicon.has-weapon-available.active::after { | |||
background: linear-gradient(90deg, | |||
rgba(255, 87, 34, 1) 0%, | |||
rgba(255, 140, 60, 1) 25%, | |||
rgba(255, 87, 34, 1) 50%, | |||
rgba(255, 140, 60, 1) 75%, | |||
rgba(255, 87, 34, 1) 100%) !important; | |||
} | |||
.top-rail.skills.weapon-mode-on .subicon.has-weapon-available.active::before { | |||
box-shadow: 0 0 14px 4px rgba(255, 87, 34, 0.4), 0 0 0 4px rgba(255, 87, 34, 0.3) !important; | |||
animation: weapon-subicon-pulse 2.5s ease-in-out infinite !important; | |||
} | |||
@keyframes weapon-subicon-border-scan { | |||
0% { | |||
background-position: 0% 50%; | |||
} | |||
100% { | |||
background-position: 400% 50%; | |||
} | |||
} | |||
@keyframes weapon-subicon-pulse { | |||
0%, | |||
100% { | |||
opacity: 0.5; | |||
box-shadow: 0 0 8px rgba(255, 80, 80, 0.25); | |||
} | |||
50% { | |||
opacity: 1; | |||
box-shadow: 0 0 16px rgba(255, 80, 80, 0.45); | |||
} | |||
} | |||
</style> | |||
Edição das 14h34min de 4 de dezembro de 2025
<script>
(function () {
const api = (window.__subskills ||= {});
const subCache = new Map();
const imagePreloadCache = new Map();
let subRail, subBar, spacer;
// Cache das skills principais (capturado na carga da página)
let cachedMainSkills = null;
// ===== HERANÇA DE ATRIBUTOS: busca dados das skills principais =====
function getMainSkillsMap() {
// Retorna cache se já foi construído E tem dados
if (cachedMainSkills && cachedMainSkills.byIndex.size > 0) {
return cachedMainSkills;
}
const maps = {
byName: new Map(),
byIndex: new Map()
};
// Busca skills com data-index (skills principais, não subskills)
const icons = document.querySelectorAll('.icon-bar .skill-icon[data-index][data-nome]');
icons.forEach(icon => {
const name = (icon.dataset.nome || ).trim();
if (!name) return;
// Extrai atributos do data-atr (formato: "pve, pvp, energy, cd")
const atrRaw = icon.dataset.atr || ;
const parts = atrRaw.split(',').map(x => (x || ).trim());
const powerpve = parts[0] && parts[0] !== '-' ? parts[0] : ;
const powerpvp = parts[1] && parts[1] !== '-' ? parts[1] : ;
const energy = parts[2] && parts[2] !== '-' ? parts[2] : ;
const cooldown = parts[3] && parts[3] !== '-' ? parts[3] : ;
// Nome original do arquivo de ícone (armazenado no dataset pelo widget de skills)
let iconFile = (icon.dataset.iconFile || ).trim();
if (!iconFile) {
const imgSrc = icon.querySelector('img')?.src || ;
const iconMatch = imgSrc.match(/(?:FilePath|images)\/([^\/?]+)$/);
iconFile = iconMatch ? decodeURIComponent(iconMatch[1]) : ;
}
// Nome original do arquivo de vídeo (caso exista)
let videoFile = (icon.dataset.videoFile || ).trim();
if (!videoFile) {
const videoUrl = icon.dataset.video || ;
const videoMatch = videoUrl.match(/FilePath\/([^&?]+)/);
videoFile = videoMatch ? decodeURIComponent(videoMatch[1]) : ;
}
const index = (icon.dataset.index || ).trim();
const data = {
name: name, // Inclui o nome para herança completa
icon: iconFile,
level: icon.dataset.level || ,
video: videoFile,
powerpve: powerpve,
powerpvp: powerpvp,
cooldown: cooldown,
energy: energy
};
// Mantém descrições caso precise como fallback extra
if (icon.dataset.descPt) data.descPt = icon.dataset.descPt;
if (icon.dataset.descEn) data.descEn = icon.dataset.descEn;
if (icon.dataset.descEs) data.descEs = icon.dataset.descEs;
if (icon.dataset.descPl) data.descPl = icon.dataset.descPl;
maps.byName.set(name, data);
if (index) {
// Guarda tanto como string quanto como número para compatibilidade
maps.byIndex.set(index, data);
maps.byIndex.set(parseInt(index, 10), data);
}
});
// Cacheia para uso futuro (importante: as skills principais não mudam)
cachedMainSkills = maps;
return maps;
}
// Aplica herança COMPLETA: se subskill só tem refM, busca TUDO da skill principal
function applyInheritance(sub, mainSkills) {
let name = (sub.name || sub.n || ).trim();
const refIndex = ((sub.refM || sub.m || sub.M || ) + ).trim();
let main = null;
// Tenta como string
if (refIndex && mainSkills.byIndex.has(refIndex)) {
main = mainSkills.byIndex.get(refIndex);
}
// Tenta como número
if (!main && refIndex) {
const numIndex = parseInt(refIndex, 10);
if (!isNaN(numIndex) && mainSkills.byIndex.has(numIndex)) {
main = mainSkills.byIndex.get(numIndex);
}
}
if (!main && name && mainSkills.byName.has(name)) {
main = mainSkills.byName.get(name);
}
// Se não tem main skill para herdar, retorna como está
if (!main) {
return sub;
}
// Se não tem nome mas tem refM, herda o nome da skill principal
if (!name && refIndex && main.name) {
name = main.name;
}
return {
...sub,
name: name || main.name || sub.name,
icon: (sub.icon && sub.icon !== 'Nada.png' && sub.icon !== ) ? sub.icon : (main.icon || 'Nada.png'),
level: sub.level || main.level,
video: sub.video || main.video,
powerpve: sub.powerpve || main.powerpve,
powerpvp: sub.powerpvp || main.powerpvp,
cooldown: sub.cooldown || main.cooldown,
energy: sub.energy || main.energy,
descPt: sub.descPt || (sub.desc_i18n?.pt) || main.descPt,
descEn: sub.descEn || (sub.desc_i18n?.en) || main.descEn,
descEs: sub.descEs || (sub.desc_i18n?.es) || main.descEs,
descPl: sub.descPl || (sub.desc_i18n?.pl) || main.descPl
};
}
function filePathURL(fileName) {
const f = encodeURIComponent((fileName || 'Nada.png').replace(/^Arquivo:|^File:/, ));
const base = (window.mw && mw.util && typeof mw.util.wikiScript === 'function')
? mw.util.wikiScript()
: (window.mw && mw.config ? (mw.config.get('wgScript') || '/index.php') : '/index.php');
return `${base}?title=Especial:FilePath/${f}`;
}
function normalizeFileURL(raw, fallback = ) {
if (!raw) return fallback;
const val = String(raw).trim();
if (!val) return fallback;
if (/^(https?:)?\/\//i.test(val) || val.startsWith('data:') || val.includes('Especial:FilePath/')) {
return val;
}
return filePathURL(val);
}
function preloadImage(iconFile) {
const url = filePathURL(iconFile || 'Nada.png');
if (imagePreloadCache.has(url)) {
return imagePreloadCache.get(url);
}
const promise = new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(url);
img.onerror = () => resolve(url);
img.src = url;
});
imagePreloadCache.set(url, promise);
return promise;
}
function getLabels() {
const skillsRoot = document.getElementById('skills');
const i18nMap = skillsRoot ? JSON.parse(skillsRoot.dataset.i18nAttrs || '{}') : {};
const raw = (document.documentElement.lang || skillsRoot?.dataset.i18nDefault || 'pt').toLowerCase();
const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
return i18nMap[lang] || i18nMap.pt || {
cooldown: 'Recarga',
energy_gain: 'Ganho de energia',
energy_cost: 'Custo de energia',
power: 'Poder',
power_pvp: 'Poder PvP',
level: 'Nível'
};
}
// Verifica se o modo weapon está ativo
function isWeaponModeOn() {
try {
return localStorage.getItem('glaWeaponEnabled') === '1';
} catch (e) {
return false;
}
}
// Retorna os atributos corretos (weapon ou normal)
function getEffectiveAttrs(s) {
const weaponOn = isWeaponModeOn();
if (weaponOn && s.weapon) {
return {
powerpve: s.weapon.powerpve || s.powerpve,
powerpvp: s.weapon.powerpvp || s.powerpvp,
energy: s.weapon.energy || s.energy,
cooldown: s.weapon.cooldown || s.cooldown
};
}
return {
powerpve: s.powerpve,
powerpvp: s.powerpvp,
energy: s.energy,
cooldown: s.cooldown
};
}
// Retorna a descrição correta (weapon ou normal)
// Aceita tanto desc_i18n quanto desc para compatibilidade
function getEffectiveDesc(s) {
const weaponOn = isWeaponModeOn();
const raw = (document.documentElement.lang || 'pt').toLowerCase();
const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
// Para weapon: aceita tanto desc_i18n quanto desc
if (weaponOn && s.weapon) {
const wDesc = s.weapon.desc_i18n || s.weapon.desc;
if (wDesc) {
return wDesc[lang] || wDesc.pt || wDesc.en || ;
}
}
// Para descrição base: aceita tanto desc_i18n quanto desc
const base = s.desc_i18n || s.desc;
if (base) {
return base[lang] || base.pt || base.en || ;
}
// Fallback para campos individuais (compatibilidade)
const descI18n = {
pt: s.descPt || ,
en: s.descEn || ,
es: s.descEs || ,
pl: s.descPl ||
};
return descI18n[lang] || descI18n.pt || ;
}
// Retorna o vídeo correto (weapon ou normal)
function getEffectiveVideo(s) {
const weaponOn = isWeaponModeOn();
if (weaponOn && s.weapon && s.weapon.video && s.weapon.video.trim() !== ) {
return s.weapon.video;
}
return s.video || ;
}
function renderSubAttrs(s, L) {
const chip = (label, val) => (val ? `
${label}${val}
` : );
const pve = (s.powerpve || ).toString().trim();
const pvp = (s.powerpvp || ).toString().trim();
const en = (s.energy || ).toString().trim();
const cd = (s.cooldown || ).toString().trim();
const rows = [
cd ? chip(L.cooldown, cd) : ,
en ? chip((en.startsWith('-') ? L.energy_cost : L.energy_gain), en.startsWith('-') ? en.replace(/^-/, ) : en.replace(/^\+?/, )) : ,
pve ? chip(L.power, pve) : ,
pvp ? chip(L.power_pvp, pvp) : ,
].filter(Boolean);
return rows.length ? `
${rows.join()}
` : ;
}
function renderFlagsRow(flags) {
const map = {
aggro: 'Enemyaggro-icon.png',
bridge: 'Bridgemaker-icon.png',
wall: 'Destroywall-icon.png',
quickcast: 'Quickcast-icon.png'
};
const arr = (flags || []).filter(Boolean);
if (!arr.length) return ;
const items = arr.map(k => `<img class="skill-flag" data-flag="${k}" alt="" src="${filePathURL(map[k])}">`).join();
return `
${items}
`;
}
function applyFlagTooltips(container) {
const skillsRoot = document.getElementById('skills');
if (!skillsRoot) return;
let pack = {};
try { pack = JSON.parse(skillsRoot.dataset.i18nFlags || '{}'); } catch (e) { }
const raw = (document.documentElement.lang || 'pt').toLowerCase();
const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
const dict = pack[lang] || pack.pt || {};
const flags = container.querySelectorAll('.skill-flags .skill-flag[data-flag]');
const tooltip = window.__globalSkillTooltip;
if (!tooltip) return;
flags.forEach(el => {
const key = el.getAttribute('data-flag');
const tip = (dict && dict[key]) || ;
if (!tip) return;
if (el.dataset.flagTipWired) return;
el.dataset.flagTipWired = '1';
el.setAttribute('aria-label', tip);
if (el.hasAttribute('title')) el.removeAttribute('title');
el.addEventListener('mouseenter', () => {
const tipEl = document.querySelector('.skill-tooltip');
if (tipEl) tipEl.classList.add('flag-tooltip');
tooltip.show(el, tip);
});
el.addEventListener('mousemove', () => {
if (performance.now() >= tooltip.lockUntil.value) {
tooltip.measureAndPos(el);
}
});
el.addEventListener('click', () => {
tooltip.lockUntil.value = performance.now() + 240;
tooltip.measureAndPos(el);
});
el.addEventListener('mouseleave', () => {
const tipEl = document.querySelector('.skill-tooltip');
if (tipEl) tipEl.classList.remove('flag-tooltip');
tooltip.hide();
});
});
}
function ensureRail(iconsBar) {
console.log('[DEBUG Subskills] ensureRail chamado, iconsBar:', iconsBar);
const rail = iconsBar.closest('.top-rail');
if (!rail) {
console.error('[DEBUG Subskills] ensureRail: .top-rail não encontrado! iconsBar:', iconsBar);
return null;
}
console.log('[DEBUG Subskills] ensureRail: rail encontrado:', rail);
if (!subRail) {
subRail = document.createElement('div');
subRail.className = 'subskills-rail collapsed hidden';
rail.appendChild(subRail);
console.log('[DEBUG Subskills] ensureRail: subRail criado e anexado:', subRail);
} else {
console.log('[DEBUG Subskills] ensureRail: subRail já existe:', subRail);
}
if (!subBar) {
subBar = document.createElement('div');
subBar.className = 'subicon-bar';
subRail.appendChild(subBar);
console.log('[DEBUG Subskills] ensureRail: subBar criado e anexado:', subBar);
} else {
console.log('[DEBUG Subskills] ensureRail: subBar já existe:', subBar);
}
if (!spacer) {
spacer = document.createElement('div');
spacer.className = 'subskills-spacer';
rail.parentNode.insertBefore(spacer, rail.nextSibling);
console.log('[DEBUG Subskills] ensureRail: spacer criado e anexado:', spacer);
}
return rail;
}
function ensureSubVideoCached(s, parentIdx, videoBox) {
const key = `sub:${parentIdx}:${(s.name || s.n || ).trim()}`;
if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) {
const precreated = window.__subskillVideosCache.get(key);
if (!subCache.has(key)) {
subCache.set(key, precreated);
}
return key;
}
if (subCache.has(key)) return key;
if (!s.video || s.video.trim() === ) return key;
const videoFileName = s.video.trim();
if (videoFileName === 'Nada.png' || videoFileName.toLowerCase().includes('nada.png')) {
return key;
}
const v = document.createElement('video');
v.className = 'skill-video';
v.dataset.sub = '1';
v.setAttribute('controls', );
v.setAttribute('preload', 'auto');
v.setAttribute('playsinline', );
Object.assign(v.style, { display: 'none', width: '100%', height: 'auto', aspectRatio: '16/9', objectFit: 'cover' });
const videoURL = normalizeFileURL(videoFileName);
if (!videoURL || videoURL.trim() === ) return key;
// Detectar formato do vídeo pela extensão
const ext = (videoURL.split('.').pop() || ).toLowerCase().split('?')[0];
const mimeTypes = {
'mp4': 'video/mp4',
'm4v': 'video/mp4',
'webm': 'video/webm',
'ogv': 'video/ogg',
'ogg': 'video/ogg',
'mov': 'video/quicktime'
};
const mimeType = mimeTypes[ext] || 'video/mp4';
const src = document.createElement('source');
src.src = videoURL;
src.type = mimeType;
v.appendChild(src);
// Fallback para Safari/iOS mais antigos
v.setAttribute('webkit-playsinline', );
v.setAttribute('x-webkit-airplay', 'allow');
videoBox.appendChild(v);
subCache.set(key, v);
if (window.__subskillVideosCache) {
window.__subskillVideosCache.set(key, v);
}
v.load();
return key;
}
// Cria vídeo de weapon em cache (para subskills com weapon) - lógica do arquivo antigo
function ensureSubVideoInCache(videoFileName, key, videoBox) {
if (subCache.has(key)) return key;
if (!videoFileName || videoFileName.trim() === ) return key;
const videoFile = videoFileName.trim();
if (videoFile === 'Nada.png' || videoFile.toLowerCase().includes('nada.png')) {
return key;
}
const v = document.createElement('video');
v.className = 'skill-video';
v.dataset.sub = '1';
v.dataset.weapon = '1';
v.setAttribute('controls', );
v.setAttribute('preload', 'auto');
v.setAttribute('playsinline', );
Object.assign(v.style, { display: 'none', width: '100%', height: 'auto', aspectRatio: '16/9', objectFit: 'cover' });
const videoURL = normalizeFileURL(videoFile);
if (!videoURL || videoURL.trim() === ) return key;
const ext = (videoURL.split('.').pop() || ).toLowerCase().split('?')[0];
const mimeTypes = {
'mp4': 'video/mp4',
'm4v': 'video/mp4',
'webm': 'video/webm',
'ogv': 'video/ogg',
'ogg': 'video/ogg',
'mov': 'video/quicktime'
};
const mimeType = mimeTypes[ext] || 'video/mp4';
const src = document.createElement('source');
src.src = videoURL;
src.type = mimeType;
v.appendChild(src);
v.setAttribute('webkit-playsinline', );
v.setAttribute('x-webkit-airplay', 'allow');
videoBox.appendChild(v);
subCache.set(key, v);
if (window.__subskillVideosCache) {
window.__subskillVideosCache.set(key, v);
}
v.load();
return key;
}
function showSubVideo(key, videoBox) {
if (!videoBox) return;
videoBox.querySelectorAll('.skill-video').forEach(v => {
try {
v.pause();
} catch (e) {
}
v.style.display = 'none';
});
let v = null;
if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) {
v = window.__subskillVideosCache.get(key);
}
if (!v) {
v = subCache.get(key);
}
if (!v) {
videoBox.style.display = 'none';
return;
}
videoBox.style.display = 'block';
v.style.display = 'block';
try {
v.currentTime = 0;
} catch (e) {
}
const suppress = document.body.dataset.suppressSkillPlay === '1';
if (!suppress) {
v.play?.().catch(() => {
});
}
}
api.refreshCurrentSubSafe = function () {
const btn = document.querySelector('.subskills-rail .subicon.active');
if (!btn) return false;
const had = document.body.dataset.suppressSkillPlay;
document.body.dataset.suppressSkillPlay = '1';
try {
btn.dispatchEvent(new Event('click', { bubbles: true }));
} finally {
if (had) document.body.dataset.suppressSkillPlay = had;
else delete document.body.dataset.suppressSkillPlay;
}
return true;
};
// Função auxiliar para aplicar classes de weapon nas subskills renderizadas
// PADRONIZADO: usa data-weapon (igual a Character.Skills.html)
const applyWeaponClassesToSubskills = () => {
const weaponOn = isWeaponModeOn();
// Busca TODAS as subskills com weapon usando o seletor correto
// Tenta múltiplos seletores para garantir que encontra
let weaponSubs = document.querySelectorAll('.subicon[data-weapon]');
if (weaponSubs.length === 0) {
// Tenta buscar em todas as subbars
const allSubbars = document.querySelectorAll('.subicon-bar');
if (allSubbars.length > 0) {
console.log('[Subskills] Nenhuma subskill encontrada com .subicon[data-weapon], tentando buscar em todas as subbars:', allSubbars.length);
allSubbars.forEach(bar => {
const subsInBar = bar.querySelectorAll('.subicon[data-weapon]');
if (subsInBar.length > 0) {
console.log('[Subskills] Subbar encontrada com', subsInBar.length, 'subskills com weapon');
weaponSubs = Array.from(weaponSubs).concat(Array.from(subsInBar));
}
});
}
}
// Remove duplicatas
weaponSubs = Array.from(new Set(Array.from(weaponSubs)));
if (weaponSubs.length > 0) {
console.log('[Subskills] applyWeaponClassesToSubskills chamado, weaponOn:', weaponOn, 'subskills com weapon:', weaponSubs.length);
}
// Só faz log detalhado se realmente encontrou subskills
if (weaponSubs.length === 0 && document.querySelectorAll('.subicon').length > 0) {
// Log adicional para debug apenas se há subicons mas nenhum com weapon
const allSubicons = document.querySelectorAll('.subicon');
console.log('[Subskills] Total de subicons no DOM:', allSubicons.length);
allSubicons.forEach((icon, idx) => {
const hasData = icon.hasAttribute('data-weapon');
const title = icon.title || icon.dataset.slug || `subicon-${idx}`;
console.log(`[Subskills] Subicon ${idx} "${title}": data-weapon=${hasData}`);
});
}
weaponSubs.forEach(el => {
if (weaponOn) {
el.classList.add('has-weapon-available');
if (el.classList.contains('active')) {
el.classList.add('weapon-equipped');
// Só log se há subskill ativa
console.log('[Subskills] Subskill ativa marcada como weapon-equipped:', el.title || el.dataset.slug);
}
} else {
el.classList.remove('has-weapon-available');
el.classList.remove('weapon-equipped');
}
});
};
api.renderBarFrom = function (el, { iconsBar, descBox, videoBox }) {
console.log('[DEBUG Subskills] renderBarFrom chamado, iconsBar:', iconsBar);
const rail = ensureRail(iconsBar);
if (!rail) {
console.error('[DEBUG Subskills] ensureRail retornou null! iconsBar:', iconsBar);
return;
}
console.log('[DEBUG Subskills] rail encontrado:', rail, 'subRail:', subRail, 'subBar:', subBar);
const rawSubs = el.getAttribute('data-subs') || ;
const rawOrder = el.getAttribute('data-suborder') || ;
const parentIdx = el.dataset.index || ;
if (!rawSubs.trim()) {
console.log('[DEBUG Subskills] Sem data-subs, ocultando subskills');
if (subRail) subRail.classList.add('collapsed');
if (subRail) subRail.classList.add('hidden');
if (subBar) subBar.innerHTML = ;
if (spacer) spacer.style.height = '0px';
return;
}
let subs;
try { subs = JSON.parse(rawSubs); } catch { subs = []; }
// DEBUG: Verifica se weapon está presente no JSON do Lua
console.log('[Subskills] Total de subskills recebidas:', subs.length);
console.log('[Subskills] JSON completo recebido do Lua:', JSON.stringify(subs, null, 2));
subs.forEach((sub, idx) => {
const subName = (sub.name || sub.n || ).trim();
if (subName === 'Karmic Jishin') {
console.log('[Subskills] Karmic Jishin do JSON Lua:', {
index: idx,
name: subName,
weapon: sub.weapon,
weaponType: typeof sub.weapon,
weaponKeys: sub.weapon ? Object.keys(sub.weapon) : null,
hasDesc_i18n: !!(sub.weapon && sub.weapon.desc_i18n),
hasDesc: !!(sub.weapon && sub.weapon.desc),
fullSub: JSON.stringify(sub, null, 2)
});
}
// Log todas as subskills com weapon
if (sub.weapon) {
console.log(`[Subskills] Subskill "${subName}" TEM WEAPON:`, {
weapon: sub.weapon,
keys: Object.keys(sub.weapon),
hasDesc_i18n: !!sub.weapon.desc_i18n,
hasDesc: !!sub.weapon.desc
});
} else {
console.log(`[Subskills] Subskill "${subName}" SEM WEAPON`);
}
});
// NORMALIZADOR: Converte weaponPacked para weapon se necessário (fallback para dados legados)
subs = subs.map(sub => {
// Se tem weaponPacked mas não tem weapon, tenta parsear
if (sub.weaponPacked && !sub.weapon && typeof sub.weaponPacked === 'string' && sub.weaponPacked.trim() !== ) {
const parts = sub.weaponPacked.split('~');
if (parts.length >= 2) {
sub.weapon = {
icon: parts[0] || 'Nada.png',
powerpve: parts[1] || null,
powerpvp: parts[2] || null,
cooldown: parts[3] || null,
video: parts[4] || ,
energy: parts[5] || null
};
// Remove valores vazios
Object.keys(sub.weapon).forEach(k => {
if (sub.weapon[k] === || sub.weapon[k] === null) {
delete sub.weapon[k];
}
});
}
}
// Garante que weapon seja objeto válido
if (sub.weapon && typeof sub.weapon === 'string') {
// Tenta parsear como JSON primeiro
try {
sub.weapon = JSON.parse(sub.weapon);
} catch {
// Se falhar, tenta formato ~
const parts = sub.weapon.split('~');
if (parts.length >= 2) {
sub.weapon = {
icon: parts[0] || 'Nada.png',
powerpve: parts[1] || null,
powerpvp: parts[2] || null,
cooldown: parts[3] || null,
video: parts[4] || ,
energy: parts[5] || null
};
Object.keys(sub.weapon).forEach(k => {
if (sub.weapon[k] === || sub.weapon[k] === null) {
delete sub.weapon[k];
}
});
} else {
sub.weapon = null;
}
}
}
return sub;
});
if (!Array.isArray(subs) || subs.length === 0) {
subRail.classList.add('collapsed');
subRail.classList.add('hidden');
subBar.innerHTML = ;
if (spacer) spacer.style.height = '0px';
return;
}
// Busca mapa das skills principais para herança
const mainSkills = getMainSkillsMap();
// Aplica herança ANTES de processar - isso resolve nome, icon, etc.
subs = subs.map(sub => applyInheritance(sub, mainSkills));
// Verifica weapon nas subskills
subs.forEach((s, i) => {
if (s.weapon) {
console.log(`[Subskills] Sub ${i} TEM WEAPON:`, JSON.stringify(s.weapon));
}
});
// Remove subskills que ficaram sem nome após herança (herança falhou)
subs = subs.filter(s => (s.name || s.n || ).trim() !== );
subRail.classList.add('hidden');
subBar.innerHTML = ;
// Usa a ordem natural das subskills após herança
let order = subs.map(s => s.name || s.n || );
if (rawOrder.trim()) {
try {
const preferred = JSON.parse(rawOrder);
if (Array.isArray(preferred) && preferred.length) {
const byName = new Map(subs.map(s => [(s.name || s.n || ), s]));
order = preferred.filter(n => byName.has(n));
}
} catch { }
}
order.forEach(nm => {
const s = subs.find(x => (x.name || x.n || ) === nm);
if (s) {
if (s.video) ensureSubVideoCached(s, parentIdx, videoBox);
if (s.icon) preloadImage(s.icon);
}
});
// DEBUG: Log todas as subskills ANTES de renderizar
console.log('[Subskills] Renderizando', subs.length, 'subskills. Verificando weapon...');
subs.forEach((sub, idx) => {
const subName = (sub.name || sub.n || ).trim();
if (sub.weapon) {
console.log(`[Subskills] Subskill ${idx} "${subName}" TEM WEAPON antes de renderizar:`, {
weapon: sub.weapon,
weaponType: typeof sub.weapon,
weaponKeys: typeof sub.weapon === 'object' ? Object.keys(sub.weapon) : null
});
} else {
if (subName === 'Karmic Jishin') {
console.warn(`[Subskills] Subskill ${idx} "${subName}" SEM WEAPON antes de renderizar!`, sub);
}
}
});
console.log('[DEBUG Subskills] Iniciando criação de subicons, order.length:', order.length, 'subs.length:', subs.length);
order.forEach(nm => {
const s = subs.find(x => (x.name || x.n || ) === nm);
if (!s) {
console.warn('[DEBUG Subskills] Subskill não encontrada para nome:', nm);
return;
}
console.log('[DEBUG Subskills] Criando subicon:', s.name || s.n || nm);
const item = document.createElement('div');
item.className = 'subicon';
item.title = s.name || nm;
const slugify = window.__skillSlugify || ((str) => (str || ).toLowerCase().replace(/[^\w]+/g, '-').replace(/^-+|-+$/g, ));
item.dataset.slug = slugify(s.name || nm);
const img = document.createElement('img');
img.alt = ;
img.src = filePathURL(s.icon || 'Nada.png');
item.appendChild(img);
// PADRONIZADO: usa data-weapon (igual a Character.Skills.html)
// Verifica weapon de forma mais robusta
const hasWeapon = s.weapon && (
(typeof s.weapon === 'object' && Object.keys(s.weapon).length > 0) ||
(typeof s.weapon === 'string' && s.weapon.trim() !== )
);
const subName = (s.name || s.n || ).trim();
if (hasWeapon) {
// Normaliza weapon se for string
let weaponObj = s.weapon;
if (typeof weaponObj === 'string') {
try {
weaponObj = JSON.parse(weaponObj);
} catch {
// Se falhar, tenta formato ~
const parts = weaponObj.split('~');
if (parts.length >= 2) {
weaponObj = {
icon: parts[0] || 'Nada.png',
powerpve: parts[1] || null,
powerpvp: parts[2] || null,
cooldown: parts[3] || null,
video: parts[4] || ,
energy: parts[5] || null
};
Object.keys(weaponObj).forEach(k => {
if (weaponObj[k] === || weaponObj[k] === null) {
delete weaponObj[k];
}
});
} else {
weaponObj = null;
}
}
}
if (weaponObj && typeof weaponObj === 'object' && Object.keys(weaponObj).length > 0) {
try {
item.dataset.weapon = JSON.stringify(weaponObj);
console.log(`[Subskills] "${subName}" - data-weapon definido, weapon:`, weaponObj);
} catch (e) {
console.error('[Subskills] Erro ao serializar weapon de subskill', subName, e);
}
// Aplica classe inicial se o toggle já está ativo
if (isWeaponModeOn()) {
item.classList.add('has-weapon-available');
}
} else {
console.warn(`[Subskills] "${subName}" - weapon inválido após normalização:`, weaponObj);
}
} else {
if (subName === 'Karmic Jishin') {
console.warn('[Subskills] Karmic Jishin - SEM WEAPON!', {
s: s,
weapon: s.weapon,
weaponType: typeof s.weapon,
weaponKeys: s.weapon ? Object.keys(s.weapon) : null
});
}
}
item.addEventListener('click', () => {
const L = getLabels();
const subName = (s.name || s.n || ).trim();
// PADRONIZADO: lê weapon diretamente do atributo data-weapon (igual a Character.Skills.html)
let subWeaponData = null;
if (item.dataset.weapon) {
try {
subWeaponData = JSON.parse(item.dataset.weapon);
} catch (e) {
console.warn('[Subskills] Erro ao parsear data-weapon:', e);
subWeaponData = null;
}
}
const hasSubWeapon = !!subWeaponData;
const weaponOn = isWeaponModeOn();
const weaponEquipped = weaponOn && hasSubWeapon && subWeaponData;
console.log('[Subskills] click handler debug:', {
subName,
weaponOn,
hasSubWeapon,
weaponEquipped,
subWeaponData: subWeaponData
});
// DEBUG: Log para Karmic Jishin
if (subName === 'Karmic Jishin') {
console.log('[Subskills Click] Karmic Jishin:', {
weaponOn,
hasSubWeapon,
weaponEquipped,
subWeaponData: subWeaponData
});
}
// Determina descrição (weapon ou normal)
const raw = (document.documentElement.lang || 'pt').toLowerCase();
const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
let chosen = ;
if (weaponEquipped && subWeaponData) {
// Descrição do weapon
const weaponDescPack = subWeaponData.desc_i18n || subWeaponData.desc || {};
chosen = weaponDescPack[lang] || weaponDescPack.pt || weaponDescPack.en || ;
} else {
// Descrição normal
const base = s.desc_i18n || s.desc || {};
chosen = base[lang] || base.pt || base.en || (s.descPt || );
}
// Determina atributos (weapon ou normal)
let attrsObj = {
powerpve: s.powerpve,
powerpvp: s.powerpvp,
energy: s.energy,
cooldown: s.cooldown
};
if (weaponEquipped && subWeaponData) {
attrsObj = {
powerpve: subWeaponData.powerpve || s.powerpve,
powerpvp: subWeaponData.powerpvp || s.powerpvp,
energy: subWeaponData.energy || s.energy,
cooldown: subWeaponData.cooldown || s.cooldown
};
}
// Level (weapon ou normal)
const level = (weaponEquipped && subWeaponData && subWeaponData.level)
? subWeaponData.level.toString().trim()
: (s.level || ).toString().trim();
let flagsHTML = ;
if (Array.isArray(s.flags) && s.flags.length > 0) {
flagsHTML = renderFlagsRow(s.flags);
}
if (descBox) {
descBox.innerHTML = `
${s.name || nm}
${level ? `
${L.level} ${level}
` : }${renderSubAttrs(attrsObj, L)}
${chosen.replace(/(.*?)/g, '$1')}
`;
}
if (videoBox) {
const oldFlags = videoBox.querySelector('.skill-flags');
if (oldFlags) oldFlags.remove();
if (flagsHTML) {
videoBox.insertAdjacentHTML('beforeend', flagsHTML);
applyFlagTooltips(videoBox);
}
}
// Vídeo (weapon ou normal) - usa valores explícitos do módulo Lua
const effectiveVideo = getEffectiveVideo(s);
console.log('[Subskills] effectiveVideo calculado:', {
subName,
weaponOn: weaponEquipped,
hasWeaponData: !!subWeaponData,
weaponVideo: subWeaponData?.video,
baseVideo: s.video,
effectiveVideo
});
if (!effectiveVideo || effectiveVideo.trim() === || effectiveVideo === 'Nada.png' || effectiveVideo.toLowerCase().includes('nada.png')) {
if (videoBox) videoBox.style.display = 'none';
console.log('[Subskills] video escolhido (nenhum)', parentIdx, subName, weaponOn, 'sem vídeo');
} else {
const videoKey = (weaponEquipped && subWeaponData && subWeaponData.video && subWeaponData.video.trim() !== )
? `sub:${parentIdx}:${subName}:weapon`
: `sub:${parentIdx}:${subName}`;
console.log('[Subskills] video escolhido', parentIdx, subName, weaponOn, effectiveVideo);
// Se tem weapon video e está ativo, precisa criar/mostrar esse vídeo
if (weaponEquipped && subWeaponData && subWeaponData.video && subWeaponData.video.trim() !== && subWeaponData.video !== 'Nada.png') {
ensureSubVideoInCache(subWeaponData.video, videoKey, videoBox);
} else if (s.video && s.video.trim() !== && s.video !== 'Nada.png') {
ensureSubVideoCached(s, parentIdx, videoBox);
}
showSubVideo(videoKey, videoBox);
}
Array.from(subBar.children).forEach(c => {
c.classList.remove('active');
c.classList.remove('weapon-equipped');
});
item.classList.add('active');
// Aplica weapon-equipped se tem weapon e está ativo (SISTEMA SEPARADO)
if (weaponEquipped) {
item.classList.add('weapon-equipped');
}
window.__lastActiveSkillIcon = item;
});
if (window.__globalSkillTooltip) {
const { show, hide, measureAndPos, lockUntil } = window.__globalSkillTooltip;
const label = item.title || ;
item.setAttribute('aria-label', label);
if (item.hasAttribute('title')) item.removeAttribute('title');
item.addEventListener('mouseenter', () => show(item, label));
item.addEventListener('mousemove', () => { if (performance.now() >= lockUntil.value) measureAndPos(item); });
item.addEventListener('click', () => { lockUntil.value = performance.now() + 240; measureAndPos(item); });
item.addEventListener('mouseleave', hide);
}
console.log('[DEBUG Subskills] Anexando subicon ao subBar:', item.title, 'subBar:', subBar);
subBar.appendChild(item);
});
// DEBUG: Verifica quantos subicons foram criados
const totalSubicons = subBar.querySelectorAll('.subicon').length;
console.log('[DEBUG Subskills] Total de subicons criados e anexados:', totalSubicons);
if (totalSubicons === 0) {
console.error('[DEBUG Subskills] ERRO: Nenhum subicon foi criado! subBar:', subBar, 'subBar.innerHTML:', subBar.innerHTML);
}
// Aplica classes de weapon nas subskills recém-renderizadas se o toggle estiver ativo
// Aplica classes de weapon DEPOIS que todos os items foram adicionados ao DOM
setTimeout(() => {
const finalCount = document.querySelectorAll('.subicon').length;
console.log('[DEBUG Subskills] Total de subicons no DOM após timeout:', finalCount);
applyWeaponClassesToSubskills();
// Dispara evento para notificar que subskills estão prontas
window.dispatchEvent(new CustomEvent('gla:subskills:ready', { detail: { count: finalCount } }));
// REGISTRA LISTENER DO EVENTO DENTRO DO renderBarFrom (após renderizar)
// Remove listener anterior se existir (evita duplicação)
if (subBar._weaponToggleListener) {
window.removeEventListener('gla:weaponToggled', subBar._weaponToggleListener);
}
// Cria novo listener que opera no subBar atual
subBar._weaponToggleListener = (e) => {
const enabled = e.detail?.enabled ?? false;
console.log('[Subskills] Evento gla:weaponToggled recebido (dentro de renderBarFrom):', enabled);
// Aplica classes usando a função auxiliar
applyWeaponClassesToSubskills();
// Atualiza a subskill ativa se houver (recarrega descrição/atributos)
setTimeout(() => {
const activeSub = subBar.querySelector('.subicon[data-weapon].active')
|| subBar.querySelector('.subicon.active');
console.log('[Subskills] Atualizando subskill ativa:', activeSub ? (activeSub.title || activeSub.dataset.slug) : 'nenhuma');
if (activeSub) {
activeSub.dispatchEvent(new Event('click', { bubbles: true }));
} else {
api.refreshCurrentSubSafe();
}
}, 50);
};
window.addEventListener('gla:weaponToggled', subBar._weaponToggleListener);
requestAnimationFrame(() => {
subRail.classList.remove('collapsed');
subRail.classList.remove('hidden');
const h = subRail.offsetHeight || 48;
if (spacer) spacer.style.height = h + 'px';
});
}, 10);
};
api.hideAll = function (videoBox) {
videoBox?.querySelectorAll('.skill-video[data-sub="1"]').forEach(v => {
try { v.pause(); } catch { }
v.style.display = 'none';
});
};
window.renderSubskillsBarFrom = function (el, ctx) { api.renderBarFrom(el, ctx); };
api.preloadAllSubskillImages = function () {
const allSkillIcons = document.querySelectorAll('.icon-bar .skill-icon[data-subs]');
const preloadPromises = [];
let totalImages = 0;
allSkillIcons.forEach(icon => {
try {
const subsRaw = icon.getAttribute('data-subs');
if (!subsRaw) return;
const subs = JSON.parse(subsRaw);
if (!Array.isArray(subs)) return;
subs.forEach(s => {
if (s && s.icon) {
preloadPromises.push(preloadImage(s.icon));
totalImages++;
}
if (s && Array.isArray(s.subs)) {
s.subs.forEach(nested => {
if (nested && nested.icon) {
preloadPromises.push(preloadImage(nested.icon));
totalImages++;
}
});
}
});
} catch (e) {
}
});
if (totalImages > 0) {
return Promise.all(preloadPromises).then(() => {
});
}
return Promise.resolve();
};
// Inicialização: constrói cache das skills principais e pré-carrega imagens
function init() {
// Constrói cache das skills principais ANTES de qualquer interação
getMainSkillsMap();
// Pré-carrega imagens das subskills
api.preloadAllSubskillImages();
// Escuta mudanças no localStorage para atualizar subskill ativa
window.addEventListener('storage', (e) => {
if (e.key === 'glaWeaponEnabled') {
setTimeout(() => api.refreshCurrentSubSafe(), 50);
}
});
// LISTENER GLOBAL: Escuta evento de toggle e aplica em todas as subskills
// Este listener SEMPRE funciona, mesmo se as subskills ainda não foram renderizadas
window.addEventListener('gla:weaponToggled', (e) => {
const enabled = e.detail?.enabled ?? false;
console.log('[Subskills] Evento gla:weaponToggled recebido (global):', enabled);
// Tenta aplicar classes imediatamente
applyWeaponClassesToSubskills();
// Se não encontrou subskills, tenta novamente após um delay (caso ainda estejam sendo renderizadas)
const weaponSubs = document.querySelectorAll('.subicon[data-subweapon]');
if (weaponSubs.length === 0) {
console.log('[Subskills] Nenhuma subskill encontrada, tentando novamente após delay...');
setTimeout(() => {
applyWeaponClassesToSubskills();
const activeSub = document.querySelector('.subicon[data-subweapon].active')
|| document.querySelector('.subicon.active');
if (activeSub) {
activeSub.dispatchEvent(new Event('click', { bubbles: true }));
}
}, 200);
} else {
// Atualiza a subskill ativa se houver
setTimeout(() => {
const activeSub = document.querySelector('.subicon[data-weapon].active')
|| document.querySelector('.subicon.active');
console.log('[Subskills] Atualizando subskill ativa:', activeSub ? (activeSub.title || activeSub.dataset.slug) : 'nenhuma');
if (activeSub) {
activeSub.dispatchEvent(new Event('click', { bubbles: true }));
} else {
api.refreshCurrentSubSafe();
}
}, 50);
}
});
// LISTENER: Escuta quando subskills estão prontas e aplica classes de weapon
window.addEventListener('gla:subskills:ready', (e) => {
console.log('[Subskills] Evento gla:subskills:ready recebido, count:', e.detail?.count);
// Aplica classes de weapon se o toggle estiver ativo
applyWeaponClassesToSubskills();
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(init, 100);
});
} else {
setTimeout(init, 100);
}
})();
</script> <style>
.subicon-bar {
display: flex;
gap: 10px;
padding: 6px 6px;
overflow-x: auto;
/* Firefox */
scrollbar-width: thin;
scrollbar-color: #ababab transparent;
}
.subicon-bar::-webkit-scrollbar {
height: 6px;
}
.subicon-bar::-webkit-scrollbar-thumb {
background: #151515;
border-radius: 3px;
}
.subicon {
width: var(--icon-size, 42px);
height: var(--icon-size, 42px);
border-radius: var(--icon-radius, 10px);
overflow: hidden;
position: relative;
flex: 0 0 auto;
cursor: pointer;
isolation: isolate;
}
.subicon img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
border-radius: inherit;
}
.subicon::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: inset 0 0 0 var(--icon-ring-w, 2px) var(--icon-idle, #cfcfcf);
pointer-events: none;
z-index: 2;
transition: box-shadow .12s ease;
}
.subicon:hover::after {
box-shadow: inset 0 0 0 var(--icon-ring-w, 2px) #e6e6e6;
}
.subicon.active::after {
box-shadow: inset 0 0 0 var(--icon-ring-w, 2px) var(--icon-active, #FFD95A);
}
.subicon.active::before {
content: "";
position: absolute;
inset: -4px;
border-radius: calc(var(--icon-radius, 10px) + 4px);
pointer-events: none;
z-index: 1;
opacity: 1;
box-shadow: 0 0 12px 3px var(--icon-active-glow, rgba(255, 217, 90, .30)),
0 0 0 calc(var(--icon-ring-w, 2px) * 2) var(--icon-active-ring, rgba(255, 217, 90, .50));
}
.top-rail.skills {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
overflow: visible;
}
.top-rail.skills .icon-bar {
margin-bottom: 0;
position: relative;
z-index: 2;
}
.subskills-rail {
position: absolute;
left: 50%;
transform: translateX(-50%);
top: calc(100% - 1px);
z-index: 3;
display: inline-flex;
justify-content: center;
align-items: center;
width: auto;
max-width: 100%;
padding: 3px 5px;
background: rgba(0, 0, 0, .38);
border: 1px solid rgba(255, 255, 255, .10);
border-top: 1px solid rgba(255, 255, 255, .08);
border-radius: 0 0 10px 10px;
box-shadow: 0 3px 9px rgba(0, 0, 0, .22);
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
overflow: hidden;
transition: opacity .14s ease, transform .14s ease;
opacity: 1;
}
.subskills-rail::before {
content: "";
position: absolute;
top: -6px;
left: 0;
right: 0;
height: 6px;
background: linear-gradient(to bottom, rgba(0, 0, 0, .20), rgba(0, 0, 0, 0));
pointer-events: none;
}
.subskills-rail.collapsed {
opacity: 0;
pointer-events: none;
transform: translate(-50%, -6px);
}
.subskills-rail.hidden {
visibility: hidden;
}
.subskills-spacer {
height: 0;
transition: height .2s ease;
}
.subskills-rail .subicon-bar {
display: inline-flex;
align-items: center;
gap: 0;
overflow-x: auto;
/* Firefox */
scrollbar-width: thin;
scrollbar-color: #ababab transparent;
}
.subskills-rail .subicon-bar::-webkit-scrollbar {
height: 6px;
}
.subskills-rail .subicon-bar::-webkit-scrollbar-thumb {
background: #151515;
border-radius: 3px;
}
.subskills-rail .subicon {
width: 42px;
height: 42px;
border-radius: 6px;
position: relative;
overflow: hidden;
flex: 0 0 auto;
cursor: pointer;
isolation: isolate;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
transform: translateZ(0);
}
.subskills-rail .subicon+.subicon {
margin-left: 4px;
}
.subskills-rail .subicon img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
border-radius: inherit;
}
.subskills-rail .subicon::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: inset 0 0 0 2px var(--icon-idle, #cfcfcf);
pointer-events: none;
z-index: 2;
transition: box-shadow .12s ease;
}
.subskills-rail .subicon:hover::after {
box-shadow: inset 0 0 0 2px #e6e6e6;
}
.subskills-rail .subicon.active::after {
box-shadow: inset 0 0 0 2px var(--icon-active, #FFD95A);
}
.video-container .skill-video {
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
object-fit: cover;
background: #000;
border-radius: 10px;
}
@media (max-width: 900px) {
.subskills-rail {
position: static;
transform: none;
margin-top: -2px;
border-top: 0;
border-radius: 0 0 10px 10px;
}
.subskills-spacer {
height: 0 !important;
}
}
.skills-rail-wrap {
position: relative;
display: block;
width: max-content;
margin: 0 auto;
}
/* Subskills com arma disponível - borda vermelha quando inativa */
.subicon.has-weapon-available:not(.active)::after {
box-shadow: inset 0 0 0 2px rgba(220, 70, 70, 0.8) !important;
}
/* Subskill com arma ATIVA - laranja/coral vibrante */
.subicon.has-weapon-available.active::after {
box-shadow: inset 0 0 0 2px #FF7043 !important;
}
.subicon.has-weapon-available.active::before {
box-shadow: 0 0 12px 3px rgba(255, 112, 67, 0.35), 0 0 0 4px rgba(255, 112, 67, 0.25) !important;
}
/* Modo arma ON - efeito animado nos subicons */
.top-rail.skills.weapon-mode-on .subicon.has-weapon-available {
position: relative;
}
.top-rail.skills.weapon-mode-on .subicon.has-weapon-available::after {
box-shadow: none !important;
background: linear-gradient(90deg,
rgba(255, 80, 80, 0.9) 0%,
rgba(255, 120, 60, 1) 25%,
rgba(255, 80, 80, 0.9) 50%,
rgba(255, 120, 60, 1) 75%,
rgba(255, 80, 80, 0.9) 100%) !important;
background-size: 400% 100% !important;
animation: weapon-subicon-border-scan 4s linear infinite !important;
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0) !important;
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0) !important;
-webkit-mask-composite: xor !important;
mask-composite: exclude !important;
padding: 2px !important;
}
.top-rail.skills.weapon-mode-on .subicon.has-weapon-available::before {
content: "";
position: absolute;
inset: -4px;
border-radius: calc(6px + 4px);
pointer-events: none;
z-index: 1;
animation: weapon-subicon-pulse 3s ease-in-out infinite !important;
}
/* Subskill ativa com arma - mais intenso */
.top-rail.skills.weapon-mode-on .subicon.has-weapon-available.active::after {
background: linear-gradient(90deg,
rgba(255, 87, 34, 1) 0%,
rgba(255, 140, 60, 1) 25%,
rgba(255, 87, 34, 1) 50%,
rgba(255, 140, 60, 1) 75%,
rgba(255, 87, 34, 1) 100%) !important;
}
.top-rail.skills.weapon-mode-on .subicon.has-weapon-available.active::before {
box-shadow: 0 0 14px 4px rgba(255, 87, 34, 0.4), 0 0 0 4px rgba(255, 87, 34, 0.3) !important;
animation: weapon-subicon-pulse 2.5s ease-in-out infinite !important;
}
@keyframes weapon-subicon-border-scan {
0% {
background-position: 0% 50%;
}
100% {
background-position: 400% 50%;
}
}
@keyframes weapon-subicon-pulse {
0%,
100% {
opacity: 0.5;
box-shadow: 0 0 8px rgba(255, 80, 80, 0.25);
}
50% {
opacity: 1;
box-shadow: 0 0 16px rgba(255, 80, 80, 0.45);
}
}
</style>