Mudanças entre as edições de "Widget:Character.Base"
Ir para navegação
Ir para pesquisar
(Criou página com '<style> →=========================== BASE STYLES & RESET ===========================: img { pointer-events: none; user-select: none;...') |
m |
||
| Linha 1: | Linha 1: | ||
< | <!-- =========================== | ||
/ | MAIN SKILLS SYSTEM | ||
=========================== --> | |||
<script> | |||
(function () { | |||
// =========================== | |||
// Utility Functions | |||
// =========================== | |||
const $ = (s, root = document) => root.querySelector(s); | |||
const $$ = (s, root = document) => Array.from(root.querySelectorAll(s)); | |||
const ensureRemoved = sel => { Array.from(document.querySelectorAll(sel)).forEach(n => n.remove()); }; | |||
const onceFlag = (el, key) => { if (!el) return false; if (el.dataset[key]) return false; el.dataset[key] = '1'; return true; }; | |||
const addOnce = (el, ev, fn) => { | |||
if (!el) return; | |||
const attr = `data-wired-${ev}`; | |||
if (el.hasAttribute(attr)) return; | |||
el.addEventListener(ev, fn); | |||
el.setAttribute(attr, '1'); | |||
}; | |||
// Extra helpers | |||
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 && window.mw.config ? (mw.config.get('wgScript') || '/index.php') : '/index.php'); | |||
return `${base}?title=Especial:FilePath/${f}`; | |||
} | |||
function getLangKey() { | |||
const skillsRoot = document.getElementById('skills'); | |||
const raw = (document.documentElement.lang || skillsRoot?.dataset.i18nDefault || 'pt').toLowerCase(); | |||
return raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt'); | |||
} | |||
function chooseDescFrom(obj) { | |||
const lang = getLangKey(); | |||
const pack = obj.desc_i18n || { pt: obj.descPt, en: obj.descEn, es: obj.descEs, pl: obj.descPl }; | |||
return (pack && (pack[lang] || pack.pt || pack.en || pack.es || pack.pl)) || (obj.desc || ''); | |||
} | |||
function renderSubAttributesFromObj(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.replace(/^\+?/, '')) : '', | |||
pve ? chip(L.power, pve) : '', | |||
pvp ? chip(L.power_pvp, pvp) : '', | |||
].filter(Boolean); | |||
return rows.length ? `<div class="attr-list">${rows.join('')}</div>` : ''; | |||
} | |||
// =========================== | |||
// DOM Setup & Initialization | |||
// =========================== | |||
const skillsTab = $('#skills'); | |||
const skinsTab = $('#skins'); | |||
const weaponTab = $('#weapon'); | |||
// Clean up existing elements | |||
ensureRemoved('.top-rail'); | |||
ensureRemoved('.content-card'); | |||
ensureRemoved('.video-placeholder'); | |||
Array.from(document.querySelectorAll('.card-skins-title, .card-skins .card-skins-title, .cardskins-title, .rail-title')).forEach(t => { | |||
if ((t.textContent || '').trim().toLowerCase().includes('skins')) { | |||
t.remove(); | |||
} | |||
}); | |||
// Setup skills tab structure | |||
if (skillsTab) { | |||
const iconBar = skillsTab.querySelector('.icon-bar'); | |||
if (iconBar) { | |||
const rail = document.createElement('div'); | |||
rail.className = 'top-rail skills'; | |||
rail.appendChild(iconBar); | |||
skillsTab.prepend(rail); | |||
} | |||
const details = skillsTab.querySelector('.skills-details'); | |||
const videoContainer = skillsTab.querySelector('.video-container'); | |||
const card = document.createElement('div'); | |||
card.className = 'content-card skills-grid'; | |||
if (details) card.appendChild(details); | |||
if (videoContainer) card.appendChild(videoContainer); | |||
skillsTab.appendChild(card); | |||
} | |||
// Setup weapon tab structure (mirror skills) | |||
if (weaponTab) { | |||
const iconBarW = weaponTab.querySelector('.icon-bar'); | |||
if (iconBarW) { | |||
const railW = document.createElement('div'); | |||
railW.className = 'top-rail weapon'; | |||
railW.appendChild(iconBarW); | |||
weaponTab.prepend(railW); | |||
} | |||
const detailsW = weaponTab.querySelector('.skills-details'); | |||
const videoContainerW = weaponTab.querySelector('.video-container'); | |||
const cardW = document.createElement('div'); | |||
cardW.className = 'content-card skills-grid'; | |||
if (detailsW) cardW.appendChild(detailsW); | |||
if (videoContainerW) cardW.appendChild(videoContainerW); | |||
weaponTab.appendChild(cardW); | |||
} | |||
// Setup skins tab structure | |||
if (skinsTab) { | |||
const wrapper = skinsTab.querySelector('.skins-carousel-wrapper'); | |||
const rail = document.createElement('div'); | |||
rail.className = 'top-rail skins'; | |||
const title = document.createElement('div'); | |||
title.className = 'rail-title'; | |||
title.textContent = 'Skins & Spotlights'; | |||
rail.appendChild(title); | |||
if (wrapper) { | |||
const card = document.createElement('div'); | |||
card.className = 'content-card'; | |||
card.appendChild(wrapper); | |||
skinsTab.prepend(rail); | |||
skinsTab.appendChild(card); | |||
} else { | |||
skinsTab.prepend(rail); | |||
} | |||
} | |||
// =========================== | |||
// Video Management | |||
// =========================== | |||
const iconsBar = $('#skills') ? $('.icon-bar', $('#skills')) : null; | |||
const iconItems = iconsBar ? Array.from(iconsBar.querySelectorAll('.skill-icon')) : []; | |||
const descBox = $('#skills') ? $('.desc-box', $('#skills')) : null; | |||
const videoBox = $('#skills') ? $('.video-container', $('#skills')) : null; | |||
const videosCache = new Map(); | |||
const nestedVideoElByIcon = new WeakMap(); | |||
const barStack = []; | |||
let initialBarSnapshot = null; | |||
let totalVideos = 0, loadedVideos = 0, autoplay = false; | |||
// Track last clicked skill/subskill for language changes | |||
window.__lastActiveSkillIcon = null; | |||
// Placeholder management | |||
let placeholder = videoBox ? videoBox.querySelector('.video-placeholder') : null; | |||
let placeholderCreatedOnLoad = false; | |||
let placeholderConsumed = false; | |||
if (!placeholder && videoBox) { | |||
placeholder = document.createElement('div'); | |||
placeholder.className = 'video-placeholder'; | |||
placeholder.innerHTML = '<img src="/images/d/d5/Icon_gla.png" alt="Carregando...">'; | |||
videoBox.appendChild(placeholder); | |||
placeholderCreatedOnLoad = true; | |||
} else if (placeholder) { | |||
placeholderCreatedOnLoad = true; | |||
} | |||
if (!placeholder) placeholderConsumed = true; | |||
const removePlaceholderSmooth = () => { | |||
if (!placeholder) return; | |||
if (placeholder.classList.contains('fade-out')) return; | |||
placeholder.classList.add('fade-out'); | |||
const onEnd = () => { | |||
try { placeholder.style.display = 'none'; } catch (e) { } | |||
placeholder.removeEventListener('transitionend', onEnd); | |||
}; | |||
placeholder.addEventListener('transitionend', onEnd, { once: true }); | |||
setTimeout(() => { try { placeholder.style.display = 'none'; } catch (e) { } }, 600); | |||
}; | |||
const showPlaceholder = () => { | |||
if (!videoBox) return; | |||
if (!placeholder || !placeholderCreatedOnLoad) return; | |||
if (placeholderConsumed) return; | |||
placeholder.classList.remove('fade-out'); | |||
placeholder.style.display = 'flex'; | |||
void placeholder.offsetWidth; | |||
}; | |||
// =========================== | |||
// Video Loading & Caching | |||
// =========================== | |||
if (iconItems.length && videoBox) { | |||
iconItems.forEach(el => { | |||
const src = (el.dataset.video || '').trim(); | |||
const idx = el.dataset.index || ''; | |||
if (!src || videosCache.has(idx)) return; | |||
totalVideos++; | |||
const v = document.createElement('video'); | |||
v.className = 'skill-video'; | |||
v.setAttribute('controls', ''); | |||
v.setAttribute('preload', 'auto'); | |||
v.setAttribute('playsinline', ''); | |||
v.style.display = 'none'; | |||
v.dataset.index = idx; | |||
v.style.width = '100%'; | |||
v.style.maxWidth = '100%'; | |||
v.style.height = 'auto'; | |||
v.style.aspectRatio = '16/9'; | |||
v.style.objectFit = 'cover'; | |||
const source = document.createElement('source'); | |||
source.src = src; | |||
source.type = 'video/webm'; | |||
v.appendChild(source); | |||
v.addEventListener('canplay', () => { | |||
loadedVideos++; | |||
if (loadedVideos === 1) { try { v.pause(); v.currentTime = 0; } catch (e) { } } | |||
const active = $('.skill-icon.active', iconsBar); | |||
if (active && active.dataset.index === idx) { | |||
if (!placeholderConsumed) { | |||
setTimeout(() => { removePlaceholderSmooth(); placeholderConsumed = true; }, 180); | |||
} | |||
} | |||
if (loadedVideos === totalVideos) autoplay = true; | |||
}); | |||
v.addEventListener('error', () => { | |||
loadedVideos++; | |||
removePlaceholderSmooth(); | |||
if (loadedVideos === totalVideos) autoplay = true; | |||
}); | |||
videoBox.appendChild(v); | |||
videosCache.set(idx, v); | |||
}); | |||
} | |||
if (totalVideos === 0 && placeholder) { | |||
placeholder.style.display = 'none'; | |||
placeholder.classList.add('fade-out'); | |||
} | |||
// =========================== | |||
// Skill Bar Wiring (root and nested) | |||
// =========================== | |||
function wireTooltipsForNewIcons() { | |||
const tip = document.querySelector('.skill-tooltip'); | |||
if (!tip) return; | |||
let lockUntil2 = 0; | |||
Array.from(document.querySelectorAll('.icon-bar .skill-icon')).forEach(icon => { | |||
if (icon.dataset.tipwired) return; | |||
icon.dataset.tipwired = '1'; | |||
const label = icon.dataset.nome || icon.dataset.name || icon.title || ''; | |||
if (label && !icon.hasAttribute('aria-label')) icon.setAttribute('aria-label', label); | |||
if (icon.hasAttribute('title')) icon.removeAttribute('title'); | |||
const img = icon.querySelector('img'); | |||
if (img) { const imgAlt = img.getAttribute('alt') || ''; const imgTitle = img.getAttribute('title') || ''; if (!label && (imgAlt || imgTitle)) icon.setAttribute('aria-label', imgAlt || imgTitle); img.setAttribute('alt', ''); if (img.hasAttribute('title')) img.removeAttribute('title'); } | |||
const measureAndPos = (el) => { | |||
if (!el || tip.getAttribute('aria-hidden') === 'true') return; | |||
tip.style.left = '0px'; tip.style.top = '0px'; | |||
const rect = el.getBoundingClientRect(); const tr = tip.getBoundingClientRect(); | |||
let left = Math.round(rect.left + (rect.width - tr.width) / 2); | |||
left = Math.max(8, Math.min(left, window.innerWidth - tr.width - 8)); | |||
const coarse = (window.matchMedia && matchMedia('(pointer: coarse)').matches) || (window.innerWidth <= 600); | |||
let top = coarse ? Math.round(rect.bottom + 10) : Math.round(rect.top - tr.height - 8); | |||
if (top < 8) top = Math.round(rect.bottom + 10); | |||
tip.style.left = left + 'px'; tip.style.top = top + 'px'; | |||
}; | |||
const show = (el, text) => { tip.textContent = text || ''; tip.setAttribute('aria-hidden', 'false'); measureAndPos(el); tip.style.opacity = '1'; }; | |||
const hide = () => { tip.setAttribute('aria-hidden', 'true'); tip.style.opacity = '0'; tip.style.left = '-9999px'; tip.style.top = '-9999px'; }; | |||
icon.addEventListener('mouseenter', () => show(icon, (icon.dataset.nome || icon.dataset.name || ''))); | |||
icon.addEventListener('mousemove', () => { if (performance.now() >= lockUntil2) measureAndPos(icon); }); | |||
icon.addEventListener('click', () => { lockUntil2 = performance.now() + 240; measureAndPos(icon); }); | |||
icon.addEventListener('mouseleave', hide); | |||
}); | |||
} | |||
function showVideoForIcon(el) { | |||
// Hide all existing videos | |||
if (videoBox) { | |||
Array.from(videoBox.querySelectorAll('video.skill-video')).forEach(v => { try { v.pause(); } catch (e) { } v.style.display = 'none'; }); | |||
} | |||
if (window.__subskills) window.__subskills.hideAll?.(videoBox); | |||
const hasIdx = !!el.dataset.index; | |||
const hasVideo = !!(el.dataset.video && el.dataset.video.trim() !== ''); | |||
if (!videoBox || !hasVideo) { | |||
if (videoBox) { videoBox.style.display = 'none'; if (placeholder) { placeholder.style.display = 'none'; placeholder.classList.add('fade-out'); } } | |||
return; | |||
} | |||
if (hasIdx && videosCache.has(el.dataset.index)) { | |||
const v = videosCache.get(el.dataset.index); | |||
videoBox.style.display = 'block'; | |||
v.style.display = 'block'; | |||
try { v.currentTime = 0; } catch (e) { } | |||
const suppress = document.body.dataset.suppressSkillPlay === '1'; | |||
if (!suppress) { if (autoplay) v.play().catch(() => { }); } else { try { v.pause(); } catch (e) { } } | |||
return; | |||
} | |||
// Nested or custom icon video | |||
let v = nestedVideoElByIcon.get(el); | |||
if (!v) { | |||
v = document.createElement('video'); | |||
v.className = 'skill-video'; | |||
v.setAttribute('controls', ''); v.setAttribute('preload', 'auto'); v.setAttribute('playsinline', ''); | |||
v.style.display = 'none'; v.style.width = '100%'; v.style.height = 'auto'; v.style.aspectRatio = '16/9'; v.style.objectFit = 'cover'; | |||
const src = document.createElement('source'); | |||
src.src = el.dataset.video; src.type = 'video/webm'; | |||
v.appendChild(src); | |||
videoBox.appendChild(v); | |||
nestedVideoElByIcon.set(el, v); | |||
} | |||
videoBox.style.display = 'block'; | |||
v.style.display = 'block'; | |||
try { v.currentTime = 0; } catch (e) { } | |||
const suppress = document.body.dataset.suppressSkillPlay === '1'; | |||
if (!suppress) { if (autoplay) v.play().catch(() => { }); } else { try { v.pause(); } catch (e) { } } | |||
} | |||
function wireClicksForCurrentBar() { | |||
const currIcons = Array.from(iconsBar.querySelectorAll('.skill-icon')); | |||
currIcons.forEach(el => { | |||
if (el.dataset.wired) return; | |||
el.dataset.wired = '1'; | |||
const label = el.dataset.nome || el.dataset.name || ''; | |||
el.setAttribute('aria-label', label); | |||
if (el.hasAttribute('title')) el.removeAttribute('title'); | |||
const img = el.querySelector('img'); if (img) { img.setAttribute('alt', ''); if (img.hasAttribute('title')) img.removeAttribute('title'); } | |||
el.addEventListener('click', () => { | |||
const skillsRoot = document.getElementById('skills'); | |||
const i18nMap = skillsRoot ? JSON.parse(skillsRoot.dataset.i18nAttrs || '{}') : {}; | |||
const L = i18nMap[getLangKey()] || i18nMap.pt || { cooldown: 'Recarga', energy_gain: 'Ganho de energia', energy_cost: 'Custo de energia', power: 'Poder', power_pvp: 'Poder PvP', level: 'Nível' }; | |||
const name = el.dataset.nome || el.dataset.name || ''; | |||
const level = (el.dataset.level || '').trim(); | |||
// Pick description from current language (respects language changes) | |||
const lang = getLangKey(); | |||
const descPack = { | |||
pt: el.dataset.descPt || '', | |||
en: el.dataset.descEn || '', | |||
es: el.dataset.descEs || '', | |||
pl: el.dataset.descPl || '' | |||
}; | |||
const chosenDesc = descPack[lang] || descPack.pt || descPack.en || descPack.es || descPack.pl || el.dataset.desc || ''; | |||
const descHtml = chosenDesc.replace(/'''(.*?)'''/g, '<b>$1</b>'); | |||
const attrsHTML = el.dataset.atr ? renderAttributes(el.dataset.atr) : (el.dataset.subattrs ? renderSubAttributesFromObj(JSON.parse(el.dataset.subattrs), L) : ''); | |||
if (descBox) { | |||
descBox.innerHTML = ` | |||
<div class="skill-title"><h3>${name}</h3></div> | |||
${level ? `<div class=\"skill-level-line\"><span class=\"attr-label\">${L.level} ${level}</span></div>` : ''} | |||
${attrsHTML} | |||
<div class="desc">${descHtml}</div>`; | |||
} | |||
// Active state | |||
currIcons.forEach(i => i.classList.remove('active')); el.classList.add('active'); if (!autoplay && loadedVideos > 0) autoplay = true; | |||
// Track for language changes | |||
window.__lastActiveSkillIcon = el; | |||
// Video | |||
showVideoForIcon(el); | |||
// Nested skills UX: clicou na skill -> troca a barra se houver subs; se for 'back', volta | |||
const subsRaw = el.dataset.subs || el.getAttribute('data-subs'); | |||
const isBack = el.dataset.back === 'true' || el.getAttribute('data-back') === 'true'; | |||
if (isBack && barStack.length) { | |||
const prev = barStack.pop(); | |||
renderBarFromItems(prev.items); | |||
const btn = document.querySelector('.skills-back-wrapper'); | |||
if (btn) btn.style.display = barStack.length ? 'block' : 'none'; | |||
return; | |||
} | |||
if (subsRaw && subsRaw.trim() !== '') { | |||
try { const subs = JSON.parse(subsRaw); pushSubBarFrom(subs, el); } catch { /* no-op */ } | |||
} | |||
}); | |||
}); | |||
// Removido: badges de +/− (abrir/voltar via clique direto no ícone) | |||
wireTooltipsForNewIcons(); | |||
} | |||
function snapshotCurrentBarItemsFromDOM() { | |||
return Array.from(iconsBar.querySelectorAll('.skill-icon')).map(el => { | |||
const img = el.querySelector('img'); | |||
const iconURL = img ? img.src : ''; | |||
const subsRaw = el.dataset.subs || el.getAttribute('data-subs') || ''; | |||
let subs = null; try { subs = subsRaw ? JSON.parse(subsRaw) : null; } catch { subs = null; } | |||
const subattrsRaw = el.dataset.subattrs || ''; | |||
return { | |||
name: el.dataset.nome || el.dataset.name || '', | |||
level: el.dataset.level || '', | |||
desc: el.dataset.desc || '', | |||
descPt: el.dataset.descPt || '', | |||
descEn: el.dataset.descEn || '', | |||
descEs: el.dataset.descEs || '', | |||
descPl: el.dataset.descPl || '', | |||
attrs: el.dataset.atr || el.dataset.attrs || '', | |||
video: el.dataset.video || '', | |||
iconURL, | |||
subs, | |||
subattrsStr: subattrsRaw, | |||
noback: el.dataset.noback || null | |||
}; | |||
}); | |||
} | |||
function ensureBackButton() { | |||
const rail = iconsBar.closest('.top-rail.skills'); if (!rail) return null; | |||
// Wrap rail in a dedicated container if not already wrapped | |||
let wrap = rail.parentElement; | |||
if (!wrap || !wrap.classList || !wrap.classList.contains('skills-rail-wrap')) { | |||
const parentNode = rail.parentNode; | |||
const newWrap = document.createElement('div'); | |||
newWrap.className = 'skills-rail-wrap'; | |||
// keep layout | |||
parentNode.insertBefore(newWrap, rail); | |||
newWrap.appendChild(rail); | |||
wrap = newWrap; | |||
} | |||
// Ensure back-wrapper exists as sibling layered behind | |||
let backWrap = wrap.querySelector('.skills-back-wrapper'); | |||
if (!backWrap) { | |||
backWrap = document.createElement('div'); | |||
backWrap.className = 'skills-back-wrapper'; | |||
const btnInner = document.createElement('button'); | |||
btnInner.className = 'skills-back'; | |||
btnInner.type = 'button'; | |||
btnInner.setAttribute('aria-label', 'Voltar'); | |||
// Use SVG double chevron for a crisper, larger icon | |||
btnInner.innerHTML = '<svg class="back-chevron" width="100%" height="100%" viewBox="0 0 36 32" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" preserveAspectRatio="xMidYMid meet"><path d="M10 2L4 16L10 30" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M20 2L14 16L20 30" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M30 2L24 16L30 30" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/></svg>'; | |||
backWrap.appendChild(btnInner); | |||
wrap.insertBefore(backWrap, rail); | |||
btnInner.addEventListener('click', () => { | |||
if (!barStack.length) return; | |||
const prev = barStack.pop(); | |||
renderBarFromItems(prev.items); | |||
backWrap.style.display = barStack.length ? 'block' : 'none'; | |||
if (!barStack.length) btnInner.classList.remove('peek'); | |||
}); | |||
} | |||
// Check if current level has noback flag | |||
const currentLevel = barStack[barStack.length - 1]; | |||
const shouldHide = currentLevel && currentLevel.noback; | |||
backWrap.style.display = (barStack.length && !shouldHide) ? 'block' : 'none'; | |||
const btnInner = backWrap.querySelector('.skills-back'); | |||
return btnInner; | |||
} | |||
function renderBarFromItems(items) { | |||
iconsBar.innerHTML = ''; | |||
items.forEach((it, idx) => { | |||
const node = document.createElement('div'); node.className = 'skill-icon'; | |||
node.dataset.nome = it.name || ''; | |||
if (it.level) node.dataset.level = it.level; | |||
if (it.desc) node.dataset.desc = it.desc; | |||
if (it.descPt) node.dataset.descPt = it.descPt; | |||
if (it.descEn) node.dataset.descEn = it.descEn; | |||
if (it.descEs) node.dataset.descEs = it.descEs; | |||
if (it.descPl) node.dataset.descPl = it.descPl; | |||
if (it.attrs) node.dataset.atr = it.attrs; | |||
if (it.video) node.dataset.video = it.video; | |||
if (it.subs) node.dataset.subs = JSON.stringify(it.subs); | |||
if (it.subattrsStr) node.dataset.subattrs = it.subattrsStr; | |||
if (it.noback) node.dataset.noback = it.noback; | |||
// mark nested icons (no dataset.index) | |||
if (!it.index) node.dataset.nested = '1'; | |||
const img = document.createElement('img'); img.alt = ''; img.src = it.iconURL || (it.icon ? filePathURL(it.icon) : ''); node.appendChild(img); | |||
iconsBar.appendChild(node); | |||
}); | |||
// Animação de entrada (igual quando entra em subskills) | |||
Array.from(iconsBar.children).forEach((c, i) => { | |||
c.style.opacity = '0'; c.style.transform = 'translateY(6px)'; | |||
requestAnimationFrame(() => { | |||
setTimeout(() => { c.style.transition = 'opacity .18s ease, transform .18s ease'; c.style.opacity = '1'; c.style.transform = 'translateY(0)'; }, i * 24); | |||
}); | |||
}); | |||
wireClicksForCurrentBar(); const b = ensureBackButton(); if (b) b.classList.add('peek'); | |||
} | } | ||
function pushSubBarFrom(subs, parentIconEl) { | |||
// Check if parent has noback flag | |||
const hasNoBack = parentIconEl && (parentIconEl.dataset.noback === 'true' || parentIconEl.getAttribute('data-noback') === 'true'); | |||
// Save current | |||
barStack.push({ items: snapshotCurrentBarItemsFromDOM(), noback: hasNoBack }); ensureBackButton(); | |||
// Build next items from JSON | |||
const skillsRoot = document.getElementById('skills'); | |||
const i18nMap = skillsRoot ? JSON.parse(skillsRoot.dataset.i18nAttrs || '{}') : {}; | |||
const L = i18nMap[getLangKey()] || i18nMap.pt || { cooldown: 'Recarga', energy_gain: 'Ganho de energia', energy_cost: 'Custo de energia', power: 'Poder', power_pvp: 'Poder PvP', level: 'Nível' }; | |||
const items = (subs || []).map(s => { | |||
const name = (s.name || s.n || '').trim(); | |||
const desc = chooseDescFrom(s).replace(/'''(.*?)'''/g, '<b>$1</b>'); | |||
const attrsHTML = renderSubAttributesFromObj(s, L); | |||
// store sub-attrs object JSON to re-render attributes later | |||
return { | |||
name, | |||
level: (s.level || '').toString().trim(), | |||
desc, | |||
descPt: (s.descPt || (s.desc_i18n && s.desc_i18n.pt) || ''), | |||
descEn: (s.descEn || (s.desc_i18n && s.desc_i18n.en) || ''), | |||
descEs: (s.descEs || (s.desc_i18n && s.desc_i18n.es) || ''), | |||
descPl: (s.descPl || (s.desc_i18n && s.desc_i18n.pl) || ''), | |||
// keep raw obj for attrs | |||
attrs: '', | |||
icon: (s.icon || 'Nada.png'), | |||
iconURL: filePathURL(s.icon || 'Nada.png'), | |||
video: s.video ? filePathURL(s.video) : '', | |||
subs: Array.isArray(s.subs) ? s.subs : null, | |||
subattrs: s, | |||
back: s.back === true ? 'true' : null | |||
}; | |||
}); | |||
// Render and attach subattrs object via data-subattrs | |||
iconsBar.innerHTML = ''; | |||
items.forEach((it, iIdx) => { | |||
const node = document.createElement('div'); node.className = 'skill-icon'; node.dataset.nested = '1'; | |||
node.dataset.nome = it.name || ''; | |||
if (it.level) node.dataset.level = it.level; | |||
if (it.desc) node.dataset.desc = it.desc; | |||
if (it.descPt) node.dataset.descPt = it.descPt; | |||
if (it.descEn) node.dataset.descEn = it.descEn; | |||
if (it.descEs) node.dataset.descEs = it.descEs; | |||
if (it.descPl) node.dataset.descPl = it.descPl; | |||
if (it.video) node.dataset.video = it.video; | |||
if (it.subs) node.dataset.subs = JSON.stringify(it.subs); | |||
if (it.subattrs) node.dataset.subattrs = JSON.stringify(it.subattrs); | |||
if (it.back) node.dataset.back = it.back; | |||
const img = document.createElement('img'); img.alt = ''; img.src = it.iconURL; node.appendChild(img); | |||
iconsBar.appendChild(node); | |||
}); | |||
// pequena animação de entrada | |||
Array.from(iconsBar.children).forEach((c, i) => { | |||
c.style.opacity = '0'; c.style.transform = 'translateY(6px)'; | |||
requestAnimationFrame(() => { | |||
setTimeout(() => { c.style.transition = 'opacity .18s ease, transform .18s ease'; c.style.opacity = '1'; c.style.transform = 'translateY(0)'; }, i * 24); | |||
}); | |||
}); | |||
wireClicksForCurrentBar(); const b2 = ensureBackButton(); if (b2) b2.classList.add('peek'); | |||
} | } | ||
// Reage à troca de idioma (emitida pelo char translator) | |||
window.addEventListener('gla:langChanged', () => { | |||
const skillsRoot = document.getElementById('skills'); | |||
const i18nMap = skillsRoot ? JSON.parse(skillsRoot.dataset.i18nAttrs || '{}') : {}; | |||
const lang = getLangKey(); | |||
// Atualiza dataset.desc dos ícones da barra atual (skills) | |||
Array.from(iconsBar.querySelectorAll('.skill-icon')).forEach(icon => { | |||
const pack = { | |||
pt: icon.dataset.descPt || '', | |||
en: icon.dataset.descEn || '', | |||
es: icon.dataset.descEs || '', | |||
pl: icon.dataset.descPl || '' | |||
}; | |||
const chosen = (pack[lang] || pack.pt || pack.en || pack.es || pack.pl || icon.dataset.desc || '').trim(); | |||
if (chosen) icon.dataset.desc = chosen; | |||
}); | |||
// Atualiza descrições salvas no stack (para futuras barras ao voltar) | |||
barStack.forEach(frame => { | |||
(frame.items || []).forEach(it => { | |||
const pack = { pt: it.descPt, en: it.descEn, es: it.descEs, pl: it.descPl }; | |||
const chosen = (pack[lang] || pack.pt || pack.en || pack.es || pack.pl || it.desc || ''); | |||
it.desc = chosen; | |||
}); | |||
}); | |||
// Atualiza dataset.desc dos ícones da weapon tab (se existir) | |||
const weaponTab = document.getElementById('weapon'); | |||
if (weaponTab) { | |||
const weaponIconBar = weaponTab.querySelector('.icon-bar'); | |||
if (weaponIconBar) { | |||
Array.from(weaponIconBar.querySelectorAll('.skill-icon')).forEach(icon => { | |||
const pack = { | |||
pt: icon.dataset.descPt || '', | |||
en: icon.dataset.descEn || '', | |||
es: icon.dataset.descEs || '', | |||
pl: icon.dataset.descPl || '' | |||
}; | |||
const chosen = (pack[lang] || pack.pt || pack.en || pack.es || pack.pl || icon.dataset.desc || '').trim(); | |||
if (chosen) icon.dataset.desc = chosen; | |||
}); | |||
} | |||
} | |||
}); | |||
// Wire initial (root) bar and add + badges | |||
wireClicksForCurrentBar(); const b0 = ensureBackButton(); if (b0) { b0.classList.add('peek'); b0.style.alignSelf = 'stretch'; } | |||
// =========================== | |||
// Tooltip System | |||
// =========================== | |||
(function initSkillTooltip() { | |||
if (document.querySelector('.skill-tooltip')) return; | |||
const tip = document.createElement('div'); | |||
tip.className = 'skill-tooltip'; | |||
tip.setAttribute('role', 'tooltip'); | |||
tip.setAttribute('aria-hidden', 'true'); | |||
document.body.appendChild(tip); | |||
const lockUntilRef = { value: 0 }; | |||
function measureAndPos(el) { | |||
if (!el || tip.getAttribute('aria-hidden') === 'true') return; | |||
tip.style.left = '0px'; | |||
tip.style.top = '0px'; | |||
const rect = el.getBoundingClientRect(); | |||
const tr = tip.getBoundingClientRect(); | |||
let left = Math.round(rect.left + (rect.width - tr.width) / 2); | |||
left = Math.max(8, Math.min(left, window.innerWidth - tr.width - 8)); | |||
const coarse = (window.matchMedia && matchMedia('(pointer: coarse)').matches) || (window.innerWidth <= 600); | |||
let top = coarse ? Math.round(rect.bottom + 10) : Math.round(rect.top - tr.height - 8); | |||
if (top < 8) top = Math.round(rect.bottom + 10); | |||
tip.style.left = left + 'px'; | |||
tip.style.top = top + 'px'; | |||
} | |||
function show(el, text) { | |||
tip.textContent = text || ''; | |||
tip.setAttribute('aria-hidden', 'false'); | |||
measureAndPos(el); | |||
tip.style.opacity = '1'; | |||
} | |||
function hide() { | |||
tip.setAttribute('aria-hidden', 'true'); | |||
tip.style.opacity = '0'; | |||
tip.style.left = '-9999px'; | |||
tip.style.top = '-9999px'; | |||
} | |||
// Export to global for subskills to use | |||
window.__globalSkillTooltip = { | |||
show, | |||
hide, | |||
measureAndPos, | |||
lockUntil: lockUntilRef | |||
}; | |||
Array.from(document.querySelectorAll('.icon-bar .skill-icon')).forEach(icon => { | |||
if (icon.dataset.tipwired) return; | |||
icon.dataset.tipwired = '1'; | |||
const label = icon.dataset.nome || icon.dataset.name || icon.title || ''; | |||
if (label && !icon.hasAttribute('aria-label')) icon.setAttribute('aria-label', label); | |||
if (icon.hasAttribute('title')) icon.removeAttribute('title'); | |||
const img = icon.querySelector('img'); | |||
if (img) { | |||
const imgAlt = img.getAttribute('alt') || ''; | |||
const imgTitle = img.getAttribute('title') || ''; | |||
if (!label && (imgAlt || imgTitle)) icon.setAttribute('aria-label', imgAlt || imgTitle); | |||
img.setAttribute('alt', ''); | |||
if (img.hasAttribute('title')) img.removeAttribute('title'); | |||
} | |||
icon.addEventListener('mouseenter', () => show(icon, label)); | |||
icon.addEventListener('mousemove', () => { if (performance.now() >= lockUntilRef.value) measureAndPos(icon); }); | |||
icon.addEventListener('click', () => { lockUntilRef.value = performance.now() + 240; measureAndPos(icon); }); | |||
icon.addEventListener('mouseleave', hide); | |||
}); | |||
// Also wire subskill icons present at load (kept in sync when sub-rail opens) | |||
Array.from(document.querySelectorAll('.subskills-rail .subicon')).forEach(sub => { | |||
if (sub.dataset.tipwired) return; | |||
sub.dataset.tipwired = '1'; | |||
const label = sub.getAttribute('title') || sub.getAttribute('aria-label') || ''; | |||
if (label && !sub.hasAttribute('aria-label')) sub.setAttribute('aria-label', label); | |||
if (sub.hasAttribute('title')) sub.removeAttribute('title'); | |||
sub.addEventListener('mouseenter', () => show(sub, label)); | |||
sub.addEventListener('mousemove', () => { if (performance.now() >= lockUntilRef.value) measureAndPos(sub); }); | |||
sub.addEventListener('click', () => { lockUntilRef.value = performance.now() + 240; measureAndPos(sub); }); | |||
sub.addEventListener('mouseleave', hide); | |||
}); | |||
window.addEventListener('scroll', () => { | |||
const visible = document.querySelector('.skill-tooltip[aria-hidden="false"]'); | |||
if (!visible) return; | |||
const target = document.querySelector('.subskills-rail .subicon:hover') | |||
|| document.querySelector('.subskills-rail .subicon.active') | |||
|| document.querySelector('.icon-bar .skill-icon:hover') | |||
|| document.querySelector('.icon-bar .skill-icon.active'); | |||
measureAndPos(target); | |||
}, true); | |||
window.addEventListener('resize', () => { | |||
const target = document.querySelector('.subskills-rail .subicon:hover') | |||
|| document.querySelector('.subskills-rail .subicon.active') | |||
|| document.querySelector('.icon-bar .skill-icon:hover') | |||
|| document.querySelector('.icon-bar .skill-icon.active'); | |||
measureAndPos(target); | |||
}); | |||
})(); | |||
// =========================== | |||
// Tab System (com transição suave de altura) | |||
// =========================== | |||
(function initTabs() { | |||
const tabs = Array.from(document.querySelectorAll('.tab-btn')); | |||
if (!tabs.length) return; | |||
const contents = Array.from(document.querySelectorAll('.tab-content')); | |||
const characterBox = document.querySelector('.character-box'); | |||
// Cria o wrapper UMA VEZ no início | |||
let wrapper = characterBox.querySelector('.tabs-height-wrapper'); | |||
if (!wrapper) { | |||
wrapper = document.createElement('div'); | |||
wrapper.className = 'tabs-height-wrapper'; | |||
// Move os conteúdos das abas para dentro do wrapper | |||
contents.forEach(c => { | |||
wrapper.appendChild(c); | |||
}); | |||
// Encontra onde inserir o wrapper (após as tabs) | |||
const tabsElement = characterBox.querySelector('.character-tabs'); | |||
if (tabsElement && tabsElement.nextSibling) { | |||
characterBox.insertBefore(wrapper, tabsElement.nextSibling); | |||
} else { | |||
characterBox.appendChild(wrapper); | |||
} | |||
} | |||
// Função para animar a altura suavemente (retorna Promise) | |||
// NOVA ESTRATÉGIA: aba já está visível mas invisível (opacity:0, visibility:hidden) | |||
async function smoothHeightTransition(fromTab, toTab) { | |||
if (!wrapper) return Promise.resolve(); | |||
// Salva o scroll atual | |||
const scrollY = window.scrollY; | |||
// Mede a altura ATUAL | |||
const currentHeight = wrapper.getBoundingClientRect().height; | |||
// A aba toTab JÁ está display:block mas invisível | |||
// Aguarda ela renderizar COMPLETAMENTE na posição real | |||
await new Promise((resolve) => { | |||
const videoContainers = toTab.querySelectorAll('.video-container'); | |||
const contentCard = toTab.querySelector('.content-card'); | |||
if (videoContainers.length === 0) { | |||
// Sem vídeos, aguarda 3 frames | |||
requestAnimationFrame(() => { | |||
requestAnimationFrame(() => { | |||
requestAnimationFrame(() => resolve()); | |||
}); | |||
}); | |||
return; | |||
} | |||
// COM vídeos: aguarda até que TUDO esteja renderizado | |||
// Faz polling da altura até estabilizar | |||
let lastHeight = 0; | |||
let stableCount = 0; | |||
const checksNeeded = 3; // Altura precisa ficar estável por 3 checks | |||
let totalChecks = 0; | |||
const maxChecks = 15; // Máximo 15 checks (750ms) | |||
function checkStability() { | |||
totalChecks++; | |||
// Mede altura atual da aba | |||
const currentTabHeight = toTab.scrollHeight; | |||
// Verifica se estabilizou | |||
if (Math.abs(currentTabHeight - lastHeight) < 5) { | |||
stableCount++; | |||
} else { | |||
stableCount = 0; // Resetar se mudou | |||
} | |||
lastHeight = currentTabHeight; | |||
// Se estável por N checks ou atingiu máximo, resolve | |||
if (stableCount >= checksNeeded || totalChecks >= maxChecks) { | |||
resolve(); | |||
} else { | |||
// Continua checando a cada 50ms | |||
setTimeout(checkStability, 50); | |||
} | |||
} | |||
// Inicia checagem após um pequeno delay | |||
setTimeout(checkStability, 50); | |||
}); | |||
// AGORA mede a altura REAL (após tudo renderizado) | |||
const nextHeight = toTab.getBoundingClientRect().height; | |||
const finalHeight = Math.max(nextHeight, 100); | |||
// Se alturas são similares (< 30px diferença), não anima | |||
if (Math.abs(finalHeight - currentHeight) < 30) { | |||
wrapper.style.height = ''; | |||
return Promise.resolve(); | |||
} | |||
// Define altura inicial fixa | |||
wrapper.style.overflow = 'hidden'; | |||
wrapper.style.height = currentHeight + 'px'; | |||
// Força reflow | |||
wrapper.offsetHeight; | |||
// Define transição | |||
wrapper.style.transition = 'height 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; | |||
// Anima para a altura FINAL (já medida corretamente) | |||
requestAnimationFrame(() => { | |||
wrapper.style.height = finalHeight + 'px'; | |||
// Não altere a posição do scroll do usuário | |||
}); | |||
// Limpa após transição | |||
return new Promise(resolve => { | |||
setTimeout(() => { | |||
wrapper.style.height = ''; | |||
wrapper.style.transition = ''; | |||
wrapper.style.overflow = ''; | |||
resolve(); | |||
}, 320); | |||
}); | |||
} | |||
tabs.forEach(btn => { | |||
if (btn.dataset.wiredTab) return; | |||
btn.dataset.wiredTab = '1'; | |||
btn.addEventListener('click', () => { | |||
const target = btn.getAttribute('data-tab'); | |||
const currentActive = contents.find(c => c.classList.contains('active')); | |||
const nextActive = contents.find(c => c.id === target); | |||
if (currentActive === nextActive) return; // Já está na aba | |||
// Previne scroll durante a transição | |||
document.body.classList.add('transitioning-tabs'); | |||
// Desativa a aba atual (fade out) | |||
if (currentActive) { | |||
currentActive.style.opacity = '0'; | |||
currentActive.style.transform = 'translateY(-8px)'; | |||
} | |||
// Delay para fade out completar | |||
setTimeout(async () => { | |||
// Remove display de TODAS as abas inativas | |||
contents.forEach(c => { | |||
if (c !== nextActive) { | |||
c.style.display = 'none'; | |||
c.classList.remove('active'); | |||
} | |||
}); | |||
// Ativa os botões | |||
tabs.forEach(b => b.classList.toggle('active', b === btn)); | |||
// MOSTRA a nova aba INVISÍVEL na posição real | |||
if (nextActive) { | |||
nextActive.classList.add('active'); | |||
nextActive.style.display = 'block'; | |||
nextActive.style.opacity = '0'; | |||
nextActive.style.visibility = 'hidden'; | |||
// Força renderização completa ANTES de medir | |||
nextActive.offsetHeight; | |||
// Pré-carrega/ativa conteúdo padrão da aba (ex.: vídeo) ANTES da medição | |||
// Assim a altura final já considera o player visível | |||
try { | |||
if (target === 'weapon' || target === 'skills') { | |||
const tabEl = document.getElementById(target); | |||
if (tabEl) { | |||
const activeIcon = tabEl.querySelector('.icon-bar .skill-icon.active'); | |||
const firstIcon = tabEl.querySelector('.icon-bar .skill-icon'); | |||
const toClick = activeIcon || firstIcon; | |||
if (toClick) { | |||
// Evita autoplay durante preparação | |||
const had = document.body.dataset.suppressSkillPlay; | |||
document.body.dataset.suppressSkillPlay = '1'; | |||
toClick.click(); | |||
// Mantém a flag até o fim da transição (limpamos mais abaixo) | |||
if (had) document.body.dataset.suppressSkillPlay = had; | |||
} | |||
} | |||
} | |||
} catch (e) { /* no-op */ } | |||
} | |||
// AGORA anima altura (com a aba já renderizada na posição correta) | |||
if (currentActive && nextActive) { | |||
await smoothHeightTransition(currentActive, nextActive); | |||
} | |||
// Após altura estar correta, MOSTRA a nova aba | |||
if (nextActive) { | |||
nextActive.style.visibility = ''; | |||
nextActive.style.transform = 'translateY(12px)'; | |||
// Anima entrada | |||
requestAnimationFrame(() => { | |||
nextActive.style.opacity = '1'; | |||
nextActive.style.transform = 'translateY(0)'; | |||
// Limpa estilos inline e classe de transição | |||
setTimeout(() => { | |||
nextActive.style.opacity = ''; | |||
nextActive.style.transform = ''; | |||
document.body.classList.remove('transitioning-tabs'); | |||
// Libera flag de autoplay suprimido (se aplicada acima) | |||
try { delete document.body.dataset.suppressSkillPlay; } catch { } | |||
}, 300); | |||
}); | |||
} | |||
}, 120); | |||
// Executa ações após a transição completa | |||
setTimeout(() => { | |||
syncDescHeight(); | |||
} | if (target === 'skins') { | ||
// Pause all cached main skill videos | |||
videosCache.forEach(v => { try { v.pause(); } catch (e) { } v.style.display = 'none'; }); | |||
// Pause all nested/subskill videos | |||
if (videoBox) { | |||
videoBox.querySelectorAll('video.skill-video').forEach(v => { | |||
try { v.pause(); } catch (e) { } | |||
v.style.display = 'none'; | |||
}); | |||
} | |||
// Hide subskill videos via API | |||
if (window.__subskills) window.__subskills.hideAll?.(videoBox); | |||
if (videoBox && placeholder) { placeholder.style.display = 'none'; placeholder.classList.add('fade-out'); } | |||
// Pause weapon videos when entering skins | |||
const weaponTabEl = document.getElementById('weapon'); | |||
if (weaponTabEl) { | |||
const wvb = weaponTabEl.querySelector('.video-container'); | |||
if (wvb) wvb.querySelectorAll('video.skill-video').forEach(v => { try { v.pause(); } catch (e) { } v.style.display = 'none'; }); | |||
} | |||
} else if (target === 'weapon') { | |||
// Initialize or refresh weapon tab | |||
const weaponTab = document.getElementById('weapon'); | |||
if (weaponTab) { | |||
const activeWeaponIcon = weaponTab.querySelector('.icon-bar .skill-icon.active'); | |||
const toClick = activeWeaponIcon || weaponTab.querySelector('.icon-bar .skill-icon'); | |||
if (toClick) toClick.click(); | |||
// Pause skills videos when entering weapon | |||
videosCache.forEach(v => { try { v.pause(); } catch (e) { } v.style.display = 'none'; }); | |||
if (videoBox) videoBox.querySelectorAll('video.skill-video').forEach(v => { try { v.pause(); } catch (e) { } v.style.display = 'none'; }); | |||
} | |||
} else { | |||
const activeIcon = document.querySelector('.icon-bar .skill-icon.active'); | |||
if (activeIcon) activeIcon.click(); | |||
// Pause weapon videos when returning to skills | |||
const weaponTabEl = document.getElementById('weapon'); | |||
if (weaponTabEl) { | |||
const wvb = weaponTabEl.querySelector('.video-container'); | |||
if (wvb) wvb.querySelectorAll('video.skill-video').forEach(v => { try { v.pause(); } catch (e) { } v.style.display = 'none'; }); | |||
} | |||
} | |||
}, 450); // Após transição completar (120ms + 300ms + buffer) | |||
}); | |||
}); | |||
})(); | |||
. | // =========================== | ||
// Skins Navigation | |||
max- | // =========================== | ||
(function initSkinsArrows() { | |||
const carousel = $('.skins-carousel'); | |||
const wrapper = $('.skins-carousel-wrapper'); | |||
const left = $('.skins-arrow.left'); | |||
const right = $('.skins-arrow.right'); | |||
if (!carousel || !left || !right || !wrapper) return; | |||
if (wrapper.dataset.wired) return; | |||
wrapper.dataset.wired = '1'; | |||
const scrollAmt = () => Math.round(carousel.clientWidth * 0.6); | |||
function setState() { | |||
const max = carousel.scrollWidth - carousel.clientWidth; | |||
const x = carousel.scrollLeft; | |||
const hasLeft = x > 5, hasRight = x < max - 5; | |||
left.style.display = hasLeft ? 'inline-block' : 'none'; | |||
right.style.display = hasRight ? 'inline-block' : 'none'; | |||
wrapper.classList.toggle('has-left', hasLeft); | |||
wrapper.classList.toggle('has-right', hasRight); | |||
carousel.style.justifyContent = (!hasLeft && !hasRight) ? 'center' : ''; | |||
} | |||
function go(dir) { | |||
const max = carousel.scrollWidth - carousel.clientWidth; | |||
const next = dir < 0 | |||
? Math.max(0, carousel.scrollLeft - scrollAmt()) | |||
: Math.min(max, carousel.scrollLeft + scrollAmt()); | |||
carousel.scrollTo({ left: next, behavior: 'smooth' }); | |||
} | |||
left.addEventListener('click', () => go(-1)); | |||
right.addEventListener('click', () => go(1)); | |||
carousel.addEventListener('scroll', setState); | |||
new ResizeObserver(setState).observe(carousel); | |||
setState(); | |||
})(); | |||
// =========================== | |||
// Utility Functions | |||
// =========================== | |||
function renderAttributes(str) { | |||
const skillsRoot = document.getElementById('skills'); | |||
const i18nMap = skillsRoot ? JSON.parse(skillsRoot.dataset.i18nAttrs || '{}') : {}; | |||
const langRaw = (document.documentElement.lang || skillsRoot?.dataset.i18nDefault || 'pt').toLowerCase(); | |||
const langKey = i18nMap[langRaw] ? langRaw : (i18nMap[langRaw.split('-')[0]] ? langRaw.split('-')[0] : 'pt'); | |||
const L = i18nMap[langKey] || i18nMap.pt || { | |||
cooldown: 'Recarga', energy_gain: 'Ganho de energia', energy_cost: 'Custo de energia', | |||
power: 'Poder', power_pvp: 'Poder PvP', level: 'Nível' | |||
}; | |||
const vals = (str || '').split(',').map(v => v.trim()); | |||
const pve = parseInt(vals[0], 10); | |||
const pvp = parseInt(vals[1], 10); | |||
const ene = parseInt(vals[2], 10); | |||
const cd = parseInt(vals[3], 10); | |||
const rows = []; | |||
if (!isNaN(cd)) rows.push([L.cooldown, cd]); | |||
if (!isNaN(ene) && ene !== 0) { | |||
const label = ene > 0 ? L.energy_gain : L.energy_cost; | |||
rows.push([label, Math.abs(ene)]); | |||
} | |||
if (!isNaN(pve)) rows.push([L.power, pve]); | |||
if (!isNaN(pvp)) rows.push([L.power_pvp, pvp]); | |||
if (!rows.length) return ''; | |||
const html = rows.map(([label, value]) => ` | |||
<div class="attr-row"> | |||
<span class="attr-label">${label}</span> | |||
<span class="attr-value">${value}</span> | |||
</div>`).join(''); | |||
return `<div class="attr-list">${html}</div>`; | |||
} | } | ||
function syncDescHeight() { | |||
// no-op on purpose | |||
} | } | ||
// =========================== | |||
// Event Listeners & Initialization | |||
// =========================== | |||
window.addEventListener('resize', syncDescHeight); | |||
if (videoBox) new ResizeObserver(syncDescHeight).observe(videoBox); | |||
iconItems.forEach(el => { | |||
const wired = !!el.dataset._sync_wired; | |||
} | if (wired) return; | ||
el.dataset._sync_wired = '1'; | |||
el.addEventListener('click', () => { | |||
Promise.resolve().then(syncDescHeight); | |||
}); | |||
}); | |||
if (iconsBar) addOnce(iconsBar, 'wheel', (e) => { | |||
if (e.deltaY) { | |||
e.preventDefault(); | |||
iconsBar.scrollLeft += e.deltaY; | |||
} | } | ||
}); | |||
// Initialize first skill | |||
. | if (iconItems.length) { | ||
const first = iconItems[0]; | |||
if (first) { | |||
if (!first.classList.contains('active')) first.classList.add('active'); | |||
setTimeout(() => first.click(), 0); | |||
} | |||
} | } | ||
// =========================== | |||
// Weapon Tab Setup (if exists) | |||
// =========================== | |||
(function initWeaponTab() { | |||
const weaponTab = document.getElementById('weapon'); | |||
if (!weaponTab) return; | |||
const weaponIconBar = weaponTab.querySelector('.icon-bar'); | |||
const weaponIconItems = weaponIconBar ? Array.from(weaponIconBar.querySelectorAll('.skill-icon')) : []; | |||
const weaponDescBox = weaponTab.querySelector('.desc-box'); | |||
const weaponVideoBox = weaponTab.querySelector('.video-container'); | |||
if (!weaponIconBar || !weaponIconItems.length) return; | |||
const weaponVideosCache = new Map(); | |||
let weaponTotalVideos = 0, weaponLoadedVideos = 0, weaponAutoplay = false; | |||
// Load weapon videos | |||
weaponIconItems.forEach(el => { | |||
const src = (el.dataset.video || '').trim(); | |||
const idx = el.dataset.index || ''; | |||
if (!src || weaponVideosCache.has(idx)) return; | |||
weaponTotalVideos++; | |||
const v = document.createElement('video'); | |||
v.className = 'skill-video'; | |||
v.setAttribute('controls', ''); | |||
v.setAttribute('preload', 'auto'); | |||
v.setAttribute('playsinline', ''); | |||
v.style.display = 'none'; | |||
v.dataset.index = idx; | |||
v.style.width = '100%'; | |||
v.style.maxWidth = '100%'; | |||
v.style.height = 'auto'; | |||
v.style.aspectRatio = '16/9'; | |||
v.style.objectFit = 'cover'; | |||
const source = document.createElement('source'); | |||
source.src = src; | |||
source.type = 'video/webm'; | |||
v.appendChild(source); | |||
v.addEventListener('canplay', () => { | |||
weaponLoadedVideos++; | |||
if (weaponLoadedVideos === weaponTotalVideos) weaponAutoplay = true; | |||
}); | |||
v.addEventListener('error', () => { | |||
weaponLoadedVideos++; | |||
if (weaponLoadedVideos === weaponTotalVideos) weaponAutoplay = true; | |||
}); | |||
weaponVideoBox.appendChild(v); | |||
weaponVideosCache.set(idx, v); | |||
}); | |||
// Wire clicks for weapon icons | |||
weaponIconItems.forEach(el => { | |||
if (el.dataset.wired) return; | |||
el.dataset.wired = '1'; | |||
const label = el.dataset.nome || el.dataset.name || ''; | |||
el.setAttribute('aria-label', label); | |||
if (el.hasAttribute('title')) el.removeAttribute('title'); | |||
const img = el.querySelector('img'); | |||
if (img) { | |||
img.setAttribute('alt', ''); | |||
if (img.hasAttribute('title')) img.removeAttribute('title'); | |||
} | |||
el.addEventListener('click', () => { | |||
const skillsRoot = document.getElementById('skills'); | |||
const i18nMap = skillsRoot ? JSON.parse(skillsRoot.dataset.i18nAttrs || '{}') : {}; | |||
const lang = getLangKey(); | |||
const L = 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' | |||
}; | |||
const name = el.dataset.nome || el.dataset.name || ''; | |||
const level = (el.dataset.level || '').trim(); | |||
const descPack = { | |||
pt: el.dataset.descPt || '', | |||
en: el.dataset.descEn || '', | |||
es: el.dataset.descEs || '', | |||
pl: el.dataset.descPl || '' | |||
}; | |||
const chosenDesc = descPack[lang] || descPack.pt || descPack.en || descPack.es || descPack.pl || el.dataset.desc || ''; | |||
const descHtml = chosenDesc.replace(/'''(.*?)'''/g, '<b>$1</b>'); | |||
const attrsHTML = el.dataset.atr ? renderAttributes(el.dataset.atr) : ''; | |||
if (weaponDescBox) { | |||
weaponDescBox.innerHTML = ` | |||
<div class="skill-title"><h3>${name}</h3></div> | |||
${level ? `<div class="skill-level-line"><span class="attr-label">${L.level} ${level}</span></div>` : ''} | |||
${attrsHTML} | |||
<div class="desc">${descHtml}</div>`; | |||
} | |||
weaponIconItems.forEach(i => i.classList.remove('active')); | |||
el.classList.add('active'); | |||
if (!weaponAutoplay && weaponLoadedVideos > 0) weaponAutoplay = true; | |||
window.__lastActiveSkillIcon = el; | |||
// Show video | |||
if (weaponVideoBox) { | |||
Array.from(weaponVideoBox.querySelectorAll('video.skill-video')).forEach(v => { | |||
try { v.pause(); } catch (e) { } | |||
v.style.display = 'none'; | |||
}); | |||
} | |||
const hasIdx = !!el.dataset.index; | |||
const hasVideo = !!(el.dataset.video && el.dataset.video.trim() !== ''); | |||
if (!weaponVideoBox || !hasVideo) { | |||
if (weaponVideoBox) weaponVideoBox.style.display = 'none'; | |||
return; | |||
} | |||
if (hasIdx && weaponVideosCache.has(el.dataset.index)) { | |||
const v = weaponVideosCache.get(el.dataset.index); | |||
weaponVideoBox.style.display = 'block'; | |||
v.style.display = 'block'; | |||
try { v.currentTime = 0; } catch (e) { } | |||
const suppress = document.body.dataset.suppressSkillPlay === '1'; | |||
if (!suppress && weaponAutoplay) v.play().catch(() => { }); | |||
} | |||
}); | |||
}); | |||
// Wire tooltips for weapon icons | |||
const tip = document.querySelector('.skill-tooltip'); | |||
if (tip) { | |||
weaponIconItems.forEach(icon => { | |||
if (icon.dataset.tipwired) return; | |||
icon.dataset.tipwired = '1'; | |||
const label = icon.dataset.nome || icon.dataset.name || icon.title || ''; | |||
if (label && !icon.hasAttribute('aria-label')) icon.setAttribute('aria-label', label); | |||
if (icon.hasAttribute('title')) icon.removeAttribute('title'); | |||
const measureAndPos = (el) => { | |||
if (!el || tip.getAttribute('aria-hidden') === 'true') return; | |||
tip.style.left = '0px'; | |||
tip.style.top = '0px'; | |||
const rect = el.getBoundingClientRect(); | |||
const tr = tip.getBoundingClientRect(); | |||
let left = Math.round(rect.left + (rect.width - tr.width) / 2); | |||
left = Math.max(8, Math.min(left, window.innerWidth - tr.width - 8)); | |||
const coarse = (window.matchMedia && matchMedia('(pointer: coarse)').matches) || (window.innerWidth <= 600); | |||
let top = coarse ? Math.round(rect.bottom + 10) : Math.round(rect.top - tr.height - 8); | |||
if (top < 8) top = Math.round(rect.bottom + 10); | |||
tip.style.left = left + 'px'; | |||
tip.style.top = top + 'px'; | |||
}; | |||
const show = (el, text) => { | |||
tip.textContent = text || ''; | |||
tip.setAttribute('aria-hidden', 'false'); | |||
measureAndPos(el); | |||
tip.style.opacity = '1'; | |||
}; | |||
const hide = () => { | |||
tip.setAttribute('aria-hidden', 'true'); | |||
tip.style.opacity = '0'; | |||
tip.style.left = '-9999px'; | |||
tip.style.top = '-9999px'; | |||
}; | |||
icon.addEventListener('mouseenter', () => show(icon, label)); | |||
icon.addEventListener('mousemove', () => measureAndPos(icon)); | |||
icon.addEventListener('click', () => { window.__globalSkillTooltip.lockUntil.value = performance.now() + 240; measureAndPos(icon); }); | |||
icon.addEventListener('mouseleave', hide); | |||
}); | |||
} | |||
// Do not pre-mark active here; activation handled on tab enter | |||
})(); | |||
// Debug logging | |||
setTimeout(() => { | |||
Array.from(document.querySelectorAll('.skill-icon')).forEach(el => { | |||
console.log('icon', el.dataset.index, 'data-video=', el.dataset.video); | |||
}); | |||
videosCache.forEach((v, idx) => { | |||
} | const src = v.querySelector('source') ? v.querySelector('source').src : v.src; | ||
</ | console.log('video element', idx, 'src=', src, 'readyState=', v.readyState); | ||
v.addEventListener('error', (ev) => { | |||
console.error('VIDEO ERROR idx=', idx, 'src=', src, 'error=', v.error); | |||
}); | |||
v.addEventListener('loadedmetadata', () => { | |||
console.log('loadedmetadata idx=', idx, 'dimensions=', v.videoWidth, 'x', v.videoHeight); | |||
}); | |||
}); | |||
}, 600); | |||
})(); | |||
</script> | |||
Edição das 19h48min de 17 de novembro de 2025
<script>
(function () {
// ===========================
// Utility Functions
// ===========================
const $ = (s, root = document) => root.querySelector(s);
const $$ = (s, root = document) => Array.from(root.querySelectorAll(s));
const ensureRemoved = sel => { Array.from(document.querySelectorAll(sel)).forEach(n => n.remove()); };
const onceFlag = (el, key) => { if (!el) return false; if (el.dataset[key]) return false; el.dataset[key] = '1'; return true; };
const addOnce = (el, ev, fn) => {
if (!el) return;
const attr = `data-wired-${ev}`;
if (el.hasAttribute(attr)) return;
el.addEventListener(ev, fn);
el.setAttribute(attr, '1');
};
// Extra helpers
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 && window.mw.config ? (mw.config.get('wgScript') || '/index.php') : '/index.php');
return `${base}?title=Especial:FilePath/${f}`;
}
function getLangKey() {
const skillsRoot = document.getElementById('skills');
const raw = (document.documentElement.lang || skillsRoot?.dataset.i18nDefault || 'pt').toLowerCase();
return raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
}
function chooseDescFrom(obj) {
const lang = getLangKey();
const pack = obj.desc_i18n || { pt: obj.descPt, en: obj.descEn, es: obj.descEs, pl: obj.descPl };
return (pack && (pack[lang] || pack.pt || pack.en || pack.es || pack.pl)) || (obj.desc || );
}
function renderSubAttributesFromObj(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.replace(/^\+?/, )) : ,
pve ? chip(L.power, pve) : ,
pvp ? chip(L.power_pvp, pvp) : ,
].filter(Boolean);
return rows.length ? `
${rows.join()}
` : ;
}
// ===========================
// DOM Setup & Initialization
// ===========================
const skillsTab = $('#skills');
const skinsTab = $('#skins');
const weaponTab = $('#weapon');
// Clean up existing elements
ensureRemoved('.top-rail');
ensureRemoved('.content-card');
ensureRemoved('.video-placeholder');
Array.from(document.querySelectorAll('.card-skins-title, .card-skins .card-skins-title, .cardskins-title, .rail-title')).forEach(t => {
if ((t.textContent || ).trim().toLowerCase().includes('skins')) {
t.remove();
}
});
// Setup skills tab structure
if (skillsTab) {
const iconBar = skillsTab.querySelector('.icon-bar');
if (iconBar) {
const rail = document.createElement('div');
rail.className = 'top-rail skills';
rail.appendChild(iconBar);
skillsTab.prepend(rail);
}
const details = skillsTab.querySelector('.skills-details');
const videoContainer = skillsTab.querySelector('.video-container');
const card = document.createElement('div');
card.className = 'content-card skills-grid';
if (details) card.appendChild(details);
if (videoContainer) card.appendChild(videoContainer);
skillsTab.appendChild(card);
}
// Setup weapon tab structure (mirror skills)
if (weaponTab) {
const iconBarW = weaponTab.querySelector('.icon-bar');
if (iconBarW) {
const railW = document.createElement('div');
railW.className = 'top-rail weapon';
railW.appendChild(iconBarW);
weaponTab.prepend(railW);
}
const detailsW = weaponTab.querySelector('.skills-details');
const videoContainerW = weaponTab.querySelector('.video-container');
const cardW = document.createElement('div');
cardW.className = 'content-card skills-grid';
if (detailsW) cardW.appendChild(detailsW);
if (videoContainerW) cardW.appendChild(videoContainerW);
weaponTab.appendChild(cardW);
}
// Setup skins tab structure
if (skinsTab) {
const wrapper = skinsTab.querySelector('.skins-carousel-wrapper');
const rail = document.createElement('div');
rail.className = 'top-rail skins';
const title = document.createElement('div');
title.className = 'rail-title';
title.textContent = 'Skins & Spotlights';
rail.appendChild(title);
if (wrapper) {
const card = document.createElement('div');
card.className = 'content-card';
card.appendChild(wrapper);
skinsTab.prepend(rail);
skinsTab.appendChild(card);
} else {
skinsTab.prepend(rail);
}
}
// ===========================
// Video Management
// ===========================
const iconsBar = $('#skills') ? $('.icon-bar', $('#skills')) : null;
const iconItems = iconsBar ? Array.from(iconsBar.querySelectorAll('.skill-icon')) : [];
const descBox = $('#skills') ? $('.desc-box', $('#skills')) : null;
const videoBox = $('#skills') ? $('.video-container', $('#skills')) : null;
const videosCache = new Map();
const nestedVideoElByIcon = new WeakMap();
const barStack = [];
let initialBarSnapshot = null;
let totalVideos = 0, loadedVideos = 0, autoplay = false;
// Track last clicked skill/subskill for language changes
window.__lastActiveSkillIcon = null;
// Placeholder management
let placeholder = videoBox ? videoBox.querySelector('.video-placeholder') : null;
let placeholderCreatedOnLoad = false;
let placeholderConsumed = false;
if (!placeholder && videoBox) {
placeholder = document.createElement('div');
placeholder.className = 'video-placeholder';
placeholder.innerHTML = '<img src="/images/d/d5/Icon_gla.png" alt="Carregando...">';
videoBox.appendChild(placeholder);
placeholderCreatedOnLoad = true;
} else if (placeholder) {
placeholderCreatedOnLoad = true;
}
if (!placeholder) placeholderConsumed = true;
const removePlaceholderSmooth = () => {
if (!placeholder) return;
if (placeholder.classList.contains('fade-out')) return;
placeholder.classList.add('fade-out');
const onEnd = () => {
try { placeholder.style.display = 'none'; } catch (e) { }
placeholder.removeEventListener('transitionend', onEnd);
};
placeholder.addEventListener('transitionend', onEnd, { once: true });
setTimeout(() => { try { placeholder.style.display = 'none'; } catch (e) { } }, 600);
};
const showPlaceholder = () => {
if (!videoBox) return;
if (!placeholder || !placeholderCreatedOnLoad) return;
if (placeholderConsumed) return;
placeholder.classList.remove('fade-out');
placeholder.style.display = 'flex';
void placeholder.offsetWidth;
};
// ===========================
// Video Loading & Caching
// ===========================
if (iconItems.length && videoBox) {
iconItems.forEach(el => {
const src = (el.dataset.video || ).trim();
const idx = el.dataset.index || ;
if (!src || videosCache.has(idx)) return;
totalVideos++;
const v = document.createElement('video');
v.className = 'skill-video';
v.setAttribute('controls', );
v.setAttribute('preload', 'auto');
v.setAttribute('playsinline', );
v.style.display = 'none';
v.dataset.index = idx;
v.style.width = '100%';
v.style.maxWidth = '100%';
v.style.height = 'auto';
v.style.aspectRatio = '16/9';
v.style.objectFit = 'cover';
const source = document.createElement('source');
source.src = src;
source.type = 'video/webm';
v.appendChild(source);
v.addEventListener('canplay', () => {
loadedVideos++;
if (loadedVideos === 1) { try { v.pause(); v.currentTime = 0; } catch (e) { } }
const active = $('.skill-icon.active', iconsBar);
if (active && active.dataset.index === idx) {
if (!placeholderConsumed) {
setTimeout(() => { removePlaceholderSmooth(); placeholderConsumed = true; }, 180);
}
}
if (loadedVideos === totalVideos) autoplay = true;
});
v.addEventListener('error', () => {
loadedVideos++;
removePlaceholderSmooth();
if (loadedVideos === totalVideos) autoplay = true;
});
videoBox.appendChild(v);
videosCache.set(idx, v);
});
}
if (totalVideos === 0 && placeholder) {
placeholder.style.display = 'none';
placeholder.classList.add('fade-out');
}
// ===========================
// Skill Bar Wiring (root and nested)
// ===========================
function wireTooltipsForNewIcons() {
const tip = document.querySelector('.skill-tooltip');
if (!tip) return;
let lockUntil2 = 0;
Array.from(document.querySelectorAll('.icon-bar .skill-icon')).forEach(icon => {
if (icon.dataset.tipwired) return;
icon.dataset.tipwired = '1';
const label = icon.dataset.nome || icon.dataset.name || icon.title || ;
if (label && !icon.hasAttribute('aria-label')) icon.setAttribute('aria-label', label);
if (icon.hasAttribute('title')) icon.removeAttribute('title');
const img = icon.querySelector('img');
if (img) { const imgAlt = img.getAttribute('alt') || ; const imgTitle = img.getAttribute('title') || ; if (!label && (imgAlt || imgTitle)) icon.setAttribute('aria-label', imgAlt || imgTitle); img.setAttribute('alt', ); if (img.hasAttribute('title')) img.removeAttribute('title'); }
const measureAndPos = (el) => {
if (!el || tip.getAttribute('aria-hidden') === 'true') return;
tip.style.left = '0px'; tip.style.top = '0px';
const rect = el.getBoundingClientRect(); const tr = tip.getBoundingClientRect();
let left = Math.round(rect.left + (rect.width - tr.width) / 2);
left = Math.max(8, Math.min(left, window.innerWidth - tr.width - 8));
const coarse = (window.matchMedia && matchMedia('(pointer: coarse)').matches) || (window.innerWidth <= 600);
let top = coarse ? Math.round(rect.bottom + 10) : Math.round(rect.top - tr.height - 8);
if (top < 8) top = Math.round(rect.bottom + 10);
tip.style.left = left + 'px'; tip.style.top = top + 'px';
};
const show = (el, text) => { tip.textContent = text || ; tip.setAttribute('aria-hidden', 'false'); measureAndPos(el); tip.style.opacity = '1'; };
const hide = () => { tip.setAttribute('aria-hidden', 'true'); tip.style.opacity = '0'; tip.style.left = '-9999px'; tip.style.top = '-9999px'; };
icon.addEventListener('mouseenter', () => show(icon, (icon.dataset.nome || icon.dataset.name || )));
icon.addEventListener('mousemove', () => { if (performance.now() >= lockUntil2) measureAndPos(icon); });
icon.addEventListener('click', () => { lockUntil2 = performance.now() + 240; measureAndPos(icon); });
icon.addEventListener('mouseleave', hide);
});
}
function showVideoForIcon(el) {
// Hide all existing videos
if (videoBox) {
Array.from(videoBox.querySelectorAll('video.skill-video')).forEach(v => { try { v.pause(); } catch (e) { } v.style.display = 'none'; });
}
if (window.__subskills) window.__subskills.hideAll?.(videoBox);
const hasIdx = !!el.dataset.index;
const hasVideo = !!(el.dataset.video && el.dataset.video.trim() !== );
if (!videoBox || !hasVideo) {
if (videoBox) { videoBox.style.display = 'none'; if (placeholder) { placeholder.style.display = 'none'; placeholder.classList.add('fade-out'); } }
return;
}
if (hasIdx && videosCache.has(el.dataset.index)) {
const v = videosCache.get(el.dataset.index);
videoBox.style.display = 'block';
v.style.display = 'block';
try { v.currentTime = 0; } catch (e) { }
const suppress = document.body.dataset.suppressSkillPlay === '1';
if (!suppress) { if (autoplay) v.play().catch(() => { }); } else { try { v.pause(); } catch (e) { } }
return;
}
// Nested or custom icon video
let v = nestedVideoElByIcon.get(el);
if (!v) {
v = document.createElement('video');
v.className = 'skill-video';
v.setAttribute('controls', ); v.setAttribute('preload', 'auto'); v.setAttribute('playsinline', );
v.style.display = 'none'; v.style.width = '100%'; v.style.height = 'auto'; v.style.aspectRatio = '16/9'; v.style.objectFit = 'cover';
const src = document.createElement('source');
src.src = el.dataset.video; src.type = 'video/webm';
v.appendChild(src);
videoBox.appendChild(v);
nestedVideoElByIcon.set(el, v);
}
videoBox.style.display = 'block';
v.style.display = 'block';
try { v.currentTime = 0; } catch (e) { }
const suppress = document.body.dataset.suppressSkillPlay === '1';
if (!suppress) { if (autoplay) v.play().catch(() => { }); } else { try { v.pause(); } catch (e) { } }
}
function wireClicksForCurrentBar() {
const currIcons = Array.from(iconsBar.querySelectorAll('.skill-icon'));
currIcons.forEach(el => {
if (el.dataset.wired) return;
el.dataset.wired = '1';
const label = el.dataset.nome || el.dataset.name || ;
el.setAttribute('aria-label', label);
if (el.hasAttribute('title')) el.removeAttribute('title');
const img = el.querySelector('img'); if (img) { img.setAttribute('alt', ); if (img.hasAttribute('title')) img.removeAttribute('title'); }
el.addEventListener('click', () => {
const skillsRoot = document.getElementById('skills');
const i18nMap = skillsRoot ? JSON.parse(skillsRoot.dataset.i18nAttrs || '{}') : {};
const L = i18nMap[getLangKey()] || i18nMap.pt || { cooldown: 'Recarga', energy_gain: 'Ganho de energia', energy_cost: 'Custo de energia', power: 'Poder', power_pvp: 'Poder PvP', level: 'Nível' };
const name = el.dataset.nome || el.dataset.name || ;
const level = (el.dataset.level || ).trim();
// Pick description from current language (respects language changes)
const lang = getLangKey();
const descPack = {
pt: el.dataset.descPt || ,
en: el.dataset.descEn || ,
es: el.dataset.descEs || ,
pl: el.dataset.descPl ||
};
const chosenDesc = descPack[lang] || descPack.pt || descPack.en || descPack.es || descPack.pl || el.dataset.desc || ;
const descHtml = chosenDesc.replace(/(.*?)/g, '$1');
const attrsHTML = el.dataset.atr ? renderAttributes(el.dataset.atr) : (el.dataset.subattrs ? renderSubAttributesFromObj(JSON.parse(el.dataset.subattrs), L) : );
if (descBox) {
descBox.innerHTML = `
${name}
${level ? `
${L.level} ${level}
` : }
${attrsHTML}
${descHtml}
`;
}
// Active state
currIcons.forEach(i => i.classList.remove('active')); el.classList.add('active'); if (!autoplay && loadedVideos > 0) autoplay = true;
// Track for language changes
window.__lastActiveSkillIcon = el;
// Video
showVideoForIcon(el);
// Nested skills UX: clicou na skill -> troca a barra se houver subs; se for 'back', volta
const subsRaw = el.dataset.subs || el.getAttribute('data-subs');
const isBack = el.dataset.back === 'true' || el.getAttribute('data-back') === 'true';
if (isBack && barStack.length) {
const prev = barStack.pop();
renderBarFromItems(prev.items);
const btn = document.querySelector('.skills-back-wrapper');
if (btn) btn.style.display = barStack.length ? 'block' : 'none';
return;
}
if (subsRaw && subsRaw.trim() !== ) {
try { const subs = JSON.parse(subsRaw); pushSubBarFrom(subs, el); } catch { /* no-op */ }
}
});
});
// Removido: badges de +/− (abrir/voltar via clique direto no ícone)
wireTooltipsForNewIcons();
}
function snapshotCurrentBarItemsFromDOM() {
return Array.from(iconsBar.querySelectorAll('.skill-icon')).map(el => {
const img = el.querySelector('img');
const iconURL = img ? img.src : ;
const subsRaw = el.dataset.subs || el.getAttribute('data-subs') || ;
let subs = null; try { subs = subsRaw ? JSON.parse(subsRaw) : null; } catch { subs = null; }
const subattrsRaw = el.dataset.subattrs || ;
return {
name: el.dataset.nome || el.dataset.name || ,
level: el.dataset.level || ,
desc: el.dataset.desc || ,
descPt: el.dataset.descPt || ,
descEn: el.dataset.descEn || ,
descEs: el.dataset.descEs || ,
descPl: el.dataset.descPl || ,
attrs: el.dataset.atr || el.dataset.attrs || ,
video: el.dataset.video || ,
iconURL,
subs,
subattrsStr: subattrsRaw,
noback: el.dataset.noback || null
};
});
}
function ensureBackButton() {
const rail = iconsBar.closest('.top-rail.skills'); if (!rail) return null;
// Wrap rail in a dedicated container if not already wrapped
let wrap = rail.parentElement;
if (!wrap || !wrap.classList || !wrap.classList.contains('skills-rail-wrap')) {
const parentNode = rail.parentNode;
const newWrap = document.createElement('div');
newWrap.className = 'skills-rail-wrap';
// keep layout
parentNode.insertBefore(newWrap, rail);
newWrap.appendChild(rail);
wrap = newWrap;
}
// Ensure back-wrapper exists as sibling layered behind
let backWrap = wrap.querySelector('.skills-back-wrapper');
if (!backWrap) {
backWrap = document.createElement('div');
backWrap.className = 'skills-back-wrapper';
const btnInner = document.createElement('button');
btnInner.className = 'skills-back';
btnInner.type = 'button';
btnInner.setAttribute('aria-label', 'Voltar');
// Use SVG double chevron for a crisper, larger icon
btnInner.innerHTML = '<svg class="back-chevron" width="100%" height="100%" viewBox="0 0 36 32" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" preserveAspectRatio="xMidYMid meet"><path d="M10 2L4 16L10 30" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M20 2L14 16L20 30" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M30 2L24 16L30 30" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
backWrap.appendChild(btnInner);
wrap.insertBefore(backWrap, rail);
btnInner.addEventListener('click', () => {
if (!barStack.length) return;
const prev = barStack.pop();
renderBarFromItems(prev.items);
backWrap.style.display = barStack.length ? 'block' : 'none';
if (!barStack.length) btnInner.classList.remove('peek');
});
}
// Check if current level has noback flag
const currentLevel = barStack[barStack.length - 1];
const shouldHide = currentLevel && currentLevel.noback;
backWrap.style.display = (barStack.length && !shouldHide) ? 'block' : 'none';
const btnInner = backWrap.querySelector('.skills-back');
return btnInner;
}
function renderBarFromItems(items) {
iconsBar.innerHTML = ;
items.forEach((it, idx) => {
const node = document.createElement('div'); node.className = 'skill-icon';
node.dataset.nome = it.name || ;
if (it.level) node.dataset.level = it.level;
if (it.desc) node.dataset.desc = it.desc;
if (it.descPt) node.dataset.descPt = it.descPt;
if (it.descEn) node.dataset.descEn = it.descEn;
if (it.descEs) node.dataset.descEs = it.descEs;
if (it.descPl) node.dataset.descPl = it.descPl;
if (it.attrs) node.dataset.atr = it.attrs;
if (it.video) node.dataset.video = it.video;
if (it.subs) node.dataset.subs = JSON.stringify(it.subs);
if (it.subattrsStr) node.dataset.subattrs = it.subattrsStr;
if (it.noback) node.dataset.noback = it.noback;
// mark nested icons (no dataset.index)
if (!it.index) node.dataset.nested = '1';
const img = document.createElement('img'); img.alt = ; img.src = it.iconURL || (it.icon ? filePathURL(it.icon) : ); node.appendChild(img);
iconsBar.appendChild(node);
});
// Animação de entrada (igual quando entra em subskills)
Array.from(iconsBar.children).forEach((c, i) => {
c.style.opacity = '0'; c.style.transform = 'translateY(6px)';
requestAnimationFrame(() => {
setTimeout(() => { c.style.transition = 'opacity .18s ease, transform .18s ease'; c.style.opacity = '1'; c.style.transform = 'translateY(0)'; }, i * 24);
});
});
wireClicksForCurrentBar(); const b = ensureBackButton(); if (b) b.classList.add('peek');
}
function pushSubBarFrom(subs, parentIconEl) {
// Check if parent has noback flag
const hasNoBack = parentIconEl && (parentIconEl.dataset.noback === 'true' || parentIconEl.getAttribute('data-noback') === 'true');
// Save current
barStack.push({ items: snapshotCurrentBarItemsFromDOM(), noback: hasNoBack }); ensureBackButton();
// Build next items from JSON
const skillsRoot = document.getElementById('skills');
const i18nMap = skillsRoot ? JSON.parse(skillsRoot.dataset.i18nAttrs || '{}') : {};
const L = i18nMap[getLangKey()] || i18nMap.pt || { cooldown: 'Recarga', energy_gain: 'Ganho de energia', energy_cost: 'Custo de energia', power: 'Poder', power_pvp: 'Poder PvP', level: 'Nível' };
const items = (subs || []).map(s => {
const name = (s.name || s.n || ).trim();
const desc = chooseDescFrom(s).replace(/(.*?)/g, '$1');
const attrsHTML = renderSubAttributesFromObj(s, L);
// store sub-attrs object JSON to re-render attributes later
return {
name,
level: (s.level || ).toString().trim(),
desc,
descPt: (s.descPt || (s.desc_i18n && s.desc_i18n.pt) || ),
descEn: (s.descEn || (s.desc_i18n && s.desc_i18n.en) || ),
descEs: (s.descEs || (s.desc_i18n && s.desc_i18n.es) || ),
descPl: (s.descPl || (s.desc_i18n && s.desc_i18n.pl) || ),
// keep raw obj for attrs
attrs: ,
icon: (s.icon || 'Nada.png'),
iconURL: filePathURL(s.icon || 'Nada.png'),
video: s.video ? filePathURL(s.video) : ,
subs: Array.isArray(s.subs) ? s.subs : null,
subattrs: s,
back: s.back === true ? 'true' : null
};
});
// Render and attach subattrs object via data-subattrs
iconsBar.innerHTML = ;
items.forEach((it, iIdx) => {
const node = document.createElement('div'); node.className = 'skill-icon'; node.dataset.nested = '1';
node.dataset.nome = it.name || ;
if (it.level) node.dataset.level = it.level;
if (it.desc) node.dataset.desc = it.desc;
if (it.descPt) node.dataset.descPt = it.descPt;
if (it.descEn) node.dataset.descEn = it.descEn;
if (it.descEs) node.dataset.descEs = it.descEs;
if (it.descPl) node.dataset.descPl = it.descPl;
if (it.video) node.dataset.video = it.video;
if (it.subs) node.dataset.subs = JSON.stringify(it.subs);
if (it.subattrs) node.dataset.subattrs = JSON.stringify(it.subattrs);
if (it.back) node.dataset.back = it.back;
const img = document.createElement('img'); img.alt = ; img.src = it.iconURL; node.appendChild(img);
iconsBar.appendChild(node);
});
// pequena animação de entrada
Array.from(iconsBar.children).forEach((c, i) => {
c.style.opacity = '0'; c.style.transform = 'translateY(6px)';
requestAnimationFrame(() => {
setTimeout(() => { c.style.transition = 'opacity .18s ease, transform .18s ease'; c.style.opacity = '1'; c.style.transform = 'translateY(0)'; }, i * 24);
});
});
wireClicksForCurrentBar(); const b2 = ensureBackButton(); if (b2) b2.classList.add('peek');
}
// Reage à troca de idioma (emitida pelo char translator)
window.addEventListener('gla:langChanged', () => {
const skillsRoot = document.getElementById('skills');
const i18nMap = skillsRoot ? JSON.parse(skillsRoot.dataset.i18nAttrs || '{}') : {};
const lang = getLangKey();
// Atualiza dataset.desc dos ícones da barra atual (skills)
Array.from(iconsBar.querySelectorAll('.skill-icon')).forEach(icon => {
const pack = {
pt: icon.dataset.descPt || ,
en: icon.dataset.descEn || ,
es: icon.dataset.descEs || ,
pl: icon.dataset.descPl ||
};
const chosen = (pack[lang] || pack.pt || pack.en || pack.es || pack.pl || icon.dataset.desc || ).trim();
if (chosen) icon.dataset.desc = chosen;
});
// Atualiza descrições salvas no stack (para futuras barras ao voltar)
barStack.forEach(frame => {
(frame.items || []).forEach(it => {
const pack = { pt: it.descPt, en: it.descEn, es: it.descEs, pl: it.descPl };
const chosen = (pack[lang] || pack.pt || pack.en || pack.es || pack.pl || it.desc || );
it.desc = chosen;
});
});
// Atualiza dataset.desc dos ícones da weapon tab (se existir)
const weaponTab = document.getElementById('weapon');
if (weaponTab) {
const weaponIconBar = weaponTab.querySelector('.icon-bar');
if (weaponIconBar) {
Array.from(weaponIconBar.querySelectorAll('.skill-icon')).forEach(icon => {
const pack = {
pt: icon.dataset.descPt || ,
en: icon.dataset.descEn || ,
es: icon.dataset.descEs || ,
pl: icon.dataset.descPl ||
};
const chosen = (pack[lang] || pack.pt || pack.en || pack.es || pack.pl || icon.dataset.desc || ).trim();
if (chosen) icon.dataset.desc = chosen;
});
}
}
});
// Wire initial (root) bar and add + badges
wireClicksForCurrentBar(); const b0 = ensureBackButton(); if (b0) { b0.classList.add('peek'); b0.style.alignSelf = 'stretch'; }
// ===========================
// Tooltip System
// ===========================
(function initSkillTooltip() {
if (document.querySelector('.skill-tooltip')) return;
const tip = document.createElement('div');
tip.className = 'skill-tooltip';
tip.setAttribute('role', 'tooltip');
tip.setAttribute('aria-hidden', 'true');
document.body.appendChild(tip);
const lockUntilRef = { value: 0 };
function measureAndPos(el) {
if (!el || tip.getAttribute('aria-hidden') === 'true') return;
tip.style.left = '0px';
tip.style.top = '0px';
const rect = el.getBoundingClientRect();
const tr = tip.getBoundingClientRect();
let left = Math.round(rect.left + (rect.width - tr.width) / 2);
left = Math.max(8, Math.min(left, window.innerWidth - tr.width - 8));
const coarse = (window.matchMedia && matchMedia('(pointer: coarse)').matches) || (window.innerWidth <= 600);
let top = coarse ? Math.round(rect.bottom + 10) : Math.round(rect.top - tr.height - 8);
if (top < 8) top = Math.round(rect.bottom + 10);
tip.style.left = left + 'px';
tip.style.top = top + 'px';
}
function show(el, text) {
tip.textContent = text || ;
tip.setAttribute('aria-hidden', 'false');
measureAndPos(el);
tip.style.opacity = '1';
}
function hide() {
tip.setAttribute('aria-hidden', 'true');
tip.style.opacity = '0';
tip.style.left = '-9999px';
tip.style.top = '-9999px';
}
// Export to global for subskills to use
window.__globalSkillTooltip = {
show,
hide,
measureAndPos,
lockUntil: lockUntilRef
};
Array.from(document.querySelectorAll('.icon-bar .skill-icon')).forEach(icon => {
if (icon.dataset.tipwired) return;
icon.dataset.tipwired = '1';
const label = icon.dataset.nome || icon.dataset.name || icon.title || ;
if (label && !icon.hasAttribute('aria-label')) icon.setAttribute('aria-label', label);
if (icon.hasAttribute('title')) icon.removeAttribute('title');
const img = icon.querySelector('img');
if (img) {
const imgAlt = img.getAttribute('alt') || ;
const imgTitle = img.getAttribute('title') || ;
if (!label && (imgAlt || imgTitle)) icon.setAttribute('aria-label', imgAlt || imgTitle);
img.setAttribute('alt', );
if (img.hasAttribute('title')) img.removeAttribute('title');
}
icon.addEventListener('mouseenter', () => show(icon, label));
icon.addEventListener('mousemove', () => { if (performance.now() >= lockUntilRef.value) measureAndPos(icon); });
icon.addEventListener('click', () => { lockUntilRef.value = performance.now() + 240; measureAndPos(icon); });
icon.addEventListener('mouseleave', hide);
});
// Also wire subskill icons present at load (kept in sync when sub-rail opens)
Array.from(document.querySelectorAll('.subskills-rail .subicon')).forEach(sub => {
if (sub.dataset.tipwired) return;
sub.dataset.tipwired = '1';
const label = sub.getAttribute('title') || sub.getAttribute('aria-label') || ;
if (label && !sub.hasAttribute('aria-label')) sub.setAttribute('aria-label', label);
if (sub.hasAttribute('title')) sub.removeAttribute('title');
sub.addEventListener('mouseenter', () => show(sub, label));
sub.addEventListener('mousemove', () => { if (performance.now() >= lockUntilRef.value) measureAndPos(sub); });
sub.addEventListener('click', () => { lockUntilRef.value = performance.now() + 240; measureAndPos(sub); });
sub.addEventListener('mouseleave', hide);
});
window.addEventListener('scroll', () => {
const visible = document.querySelector('.skill-tooltip[aria-hidden="false"]');
if (!visible) return;
const target = document.querySelector('.subskills-rail .subicon:hover')
|| document.querySelector('.subskills-rail .subicon.active')
|| document.querySelector('.icon-bar .skill-icon:hover')
|| document.querySelector('.icon-bar .skill-icon.active');
measureAndPos(target);
}, true);
window.addEventListener('resize', () => {
const target = document.querySelector('.subskills-rail .subicon:hover')
|| document.querySelector('.subskills-rail .subicon.active')
|| document.querySelector('.icon-bar .skill-icon:hover')
|| document.querySelector('.icon-bar .skill-icon.active');
measureAndPos(target);
});
})();
// ===========================
// Tab System (com transição suave de altura)
// ===========================
(function initTabs() {
const tabs = Array.from(document.querySelectorAll('.tab-btn'));
if (!tabs.length) return;
const contents = Array.from(document.querySelectorAll('.tab-content'));
const characterBox = document.querySelector('.character-box');
// Cria o wrapper UMA VEZ no início
let wrapper = characterBox.querySelector('.tabs-height-wrapper');
if (!wrapper) {
wrapper = document.createElement('div');
wrapper.className = 'tabs-height-wrapper';
// Move os conteúdos das abas para dentro do wrapper
contents.forEach(c => {
wrapper.appendChild(c);
});
// Encontra onde inserir o wrapper (após as tabs)
const tabsElement = characterBox.querySelector('.character-tabs');
if (tabsElement && tabsElement.nextSibling) {
characterBox.insertBefore(wrapper, tabsElement.nextSibling);
} else {
characterBox.appendChild(wrapper);
}
}
// Função para animar a altura suavemente (retorna Promise)
// NOVA ESTRATÉGIA: aba já está visível mas invisível (opacity:0, visibility:hidden)
async function smoothHeightTransition(fromTab, toTab) {
if (!wrapper) return Promise.resolve();
// Salva o scroll atual
const scrollY = window.scrollY;
// Mede a altura ATUAL
const currentHeight = wrapper.getBoundingClientRect().height;
// A aba toTab JÁ está display:block mas invisível
// Aguarda ela renderizar COMPLETAMENTE na posição real
await new Promise((resolve) => {
const videoContainers = toTab.querySelectorAll('.video-container');
const contentCard = toTab.querySelector('.content-card');
if (videoContainers.length === 0) {
// Sem vídeos, aguarda 3 frames
requestAnimationFrame(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => resolve());
});
});
return;
}
// COM vídeos: aguarda até que TUDO esteja renderizado
// Faz polling da altura até estabilizar
let lastHeight = 0;
let stableCount = 0;
const checksNeeded = 3; // Altura precisa ficar estável por 3 checks
let totalChecks = 0;
const maxChecks = 15; // Máximo 15 checks (750ms)
function checkStability() {
totalChecks++;
// Mede altura atual da aba
const currentTabHeight = toTab.scrollHeight;
// Verifica se estabilizou
if (Math.abs(currentTabHeight - lastHeight) < 5) {
stableCount++;
} else {
stableCount = 0; // Resetar se mudou
}
lastHeight = currentTabHeight;
// Se estável por N checks ou atingiu máximo, resolve
if (stableCount >= checksNeeded || totalChecks >= maxChecks) {
resolve();
} else {
// Continua checando a cada 50ms
setTimeout(checkStability, 50);
}
}
// Inicia checagem após um pequeno delay
setTimeout(checkStability, 50);
});
// AGORA mede a altura REAL (após tudo renderizado)
const nextHeight = toTab.getBoundingClientRect().height;
const finalHeight = Math.max(nextHeight, 100);
// Se alturas são similares (< 30px diferença), não anima
if (Math.abs(finalHeight - currentHeight) < 30) {
wrapper.style.height = ;
return Promise.resolve();
}
// Define altura inicial fixa
wrapper.style.overflow = 'hidden';
wrapper.style.height = currentHeight + 'px';
// Força reflow
wrapper.offsetHeight;
// Define transição
wrapper.style.transition = 'height 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
// Anima para a altura FINAL (já medida corretamente)
requestAnimationFrame(() => {
wrapper.style.height = finalHeight + 'px';
// Não altere a posição do scroll do usuário
});
// Limpa após transição
return new Promise(resolve => {
setTimeout(() => {
wrapper.style.height = ;
wrapper.style.transition = ;
wrapper.style.overflow = ;
resolve();
}, 320);
});
}
tabs.forEach(btn => {
if (btn.dataset.wiredTab) return;
btn.dataset.wiredTab = '1';
btn.addEventListener('click', () => {
const target = btn.getAttribute('data-tab');
const currentActive = contents.find(c => c.classList.contains('active'));
const nextActive = contents.find(c => c.id === target);
if (currentActive === nextActive) return; // Já está na aba
// Previne scroll durante a transição
document.body.classList.add('transitioning-tabs');
// Desativa a aba atual (fade out)
if (currentActive) {
currentActive.style.opacity = '0';
currentActive.style.transform = 'translateY(-8px)';
}
// Delay para fade out completar
setTimeout(async () => {
// Remove display de TODAS as abas inativas
contents.forEach(c => {
if (c !== nextActive) {
c.style.display = 'none';
c.classList.remove('active');
}
});
// Ativa os botões
tabs.forEach(b => b.classList.toggle('active', b === btn));
// MOSTRA a nova aba INVISÍVEL na posição real
if (nextActive) {
nextActive.classList.add('active');
nextActive.style.display = 'block';
nextActive.style.opacity = '0';
nextActive.style.visibility = 'hidden';
// Força renderização completa ANTES de medir
nextActive.offsetHeight;
// Pré-carrega/ativa conteúdo padrão da aba (ex.: vídeo) ANTES da medição
// Assim a altura final já considera o player visível
try {
if (target === 'weapon' || target === 'skills') {
const tabEl = document.getElementById(target);
if (tabEl) {
const activeIcon = tabEl.querySelector('.icon-bar .skill-icon.active');
const firstIcon = tabEl.querySelector('.icon-bar .skill-icon');
const toClick = activeIcon || firstIcon;
if (toClick) {
// Evita autoplay durante preparação
const had = document.body.dataset.suppressSkillPlay;
document.body.dataset.suppressSkillPlay = '1';
toClick.click();
// Mantém a flag até o fim da transição (limpamos mais abaixo)
if (had) document.body.dataset.suppressSkillPlay = had;
}
}
}
} catch (e) { /* no-op */ }
}
// AGORA anima altura (com a aba já renderizada na posição correta)
if (currentActive && nextActive) {
await smoothHeightTransition(currentActive, nextActive);
}
// Após altura estar correta, MOSTRA a nova aba
if (nextActive) {
nextActive.style.visibility = ;
nextActive.style.transform = 'translateY(12px)';
// Anima entrada
requestAnimationFrame(() => {
nextActive.style.opacity = '1';
nextActive.style.transform = 'translateY(0)';
// Limpa estilos inline e classe de transição
setTimeout(() => {
nextActive.style.opacity = ;
nextActive.style.transform = ;
document.body.classList.remove('transitioning-tabs');
// Libera flag de autoplay suprimido (se aplicada acima)
try { delete document.body.dataset.suppressSkillPlay; } catch { }
}, 300);
});
}
}, 120);
// Executa ações após a transição completa
setTimeout(() => {
syncDescHeight();
if (target === 'skins') {
// Pause all cached main skill videos
videosCache.forEach(v => { try { v.pause(); } catch (e) { } v.style.display = 'none'; });
// Pause all nested/subskill videos
if (videoBox) {
videoBox.querySelectorAll('video.skill-video').forEach(v => {
try { v.pause(); } catch (e) { }
v.style.display = 'none';
});
}
// Hide subskill videos via API
if (window.__subskills) window.__subskills.hideAll?.(videoBox);
if (videoBox && placeholder) { placeholder.style.display = 'none'; placeholder.classList.add('fade-out'); }
// Pause weapon videos when entering skins
const weaponTabEl = document.getElementById('weapon');
if (weaponTabEl) {
const wvb = weaponTabEl.querySelector('.video-container');
if (wvb) wvb.querySelectorAll('video.skill-video').forEach(v => { try { v.pause(); } catch (e) { } v.style.display = 'none'; });
}
} else if (target === 'weapon') {
// Initialize or refresh weapon tab
const weaponTab = document.getElementById('weapon');
if (weaponTab) {
const activeWeaponIcon = weaponTab.querySelector('.icon-bar .skill-icon.active');
const toClick = activeWeaponIcon || weaponTab.querySelector('.icon-bar .skill-icon');
if (toClick) toClick.click();
// Pause skills videos when entering weapon
videosCache.forEach(v => { try { v.pause(); } catch (e) { } v.style.display = 'none'; });
if (videoBox) videoBox.querySelectorAll('video.skill-video').forEach(v => { try { v.pause(); } catch (e) { } v.style.display = 'none'; });
}
} else {
const activeIcon = document.querySelector('.icon-bar .skill-icon.active');
if (activeIcon) activeIcon.click();
// Pause weapon videos when returning to skills
const weaponTabEl = document.getElementById('weapon');
if (weaponTabEl) {
const wvb = weaponTabEl.querySelector('.video-container');
if (wvb) wvb.querySelectorAll('video.skill-video').forEach(v => { try { v.pause(); } catch (e) { } v.style.display = 'none'; });
}
}
}, 450); // Após transição completar (120ms + 300ms + buffer)
});
});
})();
// ===========================
// Skins Navigation
// ===========================
(function initSkinsArrows() {
const carousel = $('.skins-carousel');
const wrapper = $('.skins-carousel-wrapper');
const left = $('.skins-arrow.left');
const right = $('.skins-arrow.right');
if (!carousel || !left || !right || !wrapper) return;
if (wrapper.dataset.wired) return;
wrapper.dataset.wired = '1';
const scrollAmt = () => Math.round(carousel.clientWidth * 0.6);
function setState() {
const max = carousel.scrollWidth - carousel.clientWidth;
const x = carousel.scrollLeft;
const hasLeft = x > 5, hasRight = x < max - 5;
left.style.display = hasLeft ? 'inline-block' : 'none';
right.style.display = hasRight ? 'inline-block' : 'none';
wrapper.classList.toggle('has-left', hasLeft);
wrapper.classList.toggle('has-right', hasRight);
carousel.style.justifyContent = (!hasLeft && !hasRight) ? 'center' : ;
}
function go(dir) {
const max = carousel.scrollWidth - carousel.clientWidth;
const next = dir < 0
? Math.max(0, carousel.scrollLeft - scrollAmt())
: Math.min(max, carousel.scrollLeft + scrollAmt());
carousel.scrollTo({ left: next, behavior: 'smooth' });
}
left.addEventListener('click', () => go(-1));
right.addEventListener('click', () => go(1));
carousel.addEventListener('scroll', setState);
new ResizeObserver(setState).observe(carousel);
setState();
})();
// ===========================
// Utility Functions
// ===========================
function renderAttributes(str) {
const skillsRoot = document.getElementById('skills');
const i18nMap = skillsRoot ? JSON.parse(skillsRoot.dataset.i18nAttrs || '{}') : {};
const langRaw = (document.documentElement.lang || skillsRoot?.dataset.i18nDefault || 'pt').toLowerCase();
const langKey = i18nMap[langRaw] ? langRaw : (i18nMap[langRaw.split('-')[0]] ? langRaw.split('-')[0] : 'pt');
const L = i18nMap[langKey] || i18nMap.pt || {
cooldown: 'Recarga', energy_gain: 'Ganho de energia', energy_cost: 'Custo de energia',
power: 'Poder', power_pvp: 'Poder PvP', level: 'Nível'
};
const vals = (str || ).split(',').map(v => v.trim());
const pve = parseInt(vals[0], 10);
const pvp = parseInt(vals[1], 10);
const ene = parseInt(vals[2], 10);
const cd = parseInt(vals[3], 10);
const rows = [];
if (!isNaN(cd)) rows.push([L.cooldown, cd]);
if (!isNaN(ene) && ene !== 0) {
const label = ene > 0 ? L.energy_gain : L.energy_cost;
rows.push([label, Math.abs(ene)]);
}
if (!isNaN(pve)) rows.push([L.power, pve]);
if (!isNaN(pvp)) rows.push([L.power_pvp, pvp]);
if (!rows.length) return ;
const html = rows.map(([label, value]) => `
${label} ${value}
`).join(); return `
${html}
`;
}
function syncDescHeight() {
// no-op on purpose
}
// ===========================
// Event Listeners & Initialization
// ===========================
window.addEventListener('resize', syncDescHeight);
if (videoBox) new ResizeObserver(syncDescHeight).observe(videoBox);
iconItems.forEach(el => {
const wired = !!el.dataset._sync_wired;
if (wired) return;
el.dataset._sync_wired = '1';
el.addEventListener('click', () => {
Promise.resolve().then(syncDescHeight);
});
});
if (iconsBar) addOnce(iconsBar, 'wheel', (e) => {
if (e.deltaY) {
e.preventDefault();
iconsBar.scrollLeft += e.deltaY;
}
});
// Initialize first skill
if (iconItems.length) {
const first = iconItems[0];
if (first) {
if (!first.classList.contains('active')) first.classList.add('active');
setTimeout(() => first.click(), 0);
}
}
// ===========================
// Weapon Tab Setup (if exists)
// ===========================
(function initWeaponTab() {
const weaponTab = document.getElementById('weapon');
if (!weaponTab) return;
const weaponIconBar = weaponTab.querySelector('.icon-bar');
const weaponIconItems = weaponIconBar ? Array.from(weaponIconBar.querySelectorAll('.skill-icon')) : [];
const weaponDescBox = weaponTab.querySelector('.desc-box');
const weaponVideoBox = weaponTab.querySelector('.video-container');
if (!weaponIconBar || !weaponIconItems.length) return;
const weaponVideosCache = new Map();
let weaponTotalVideos = 0, weaponLoadedVideos = 0, weaponAutoplay = false;
// Load weapon videos
weaponIconItems.forEach(el => {
const src = (el.dataset.video || ).trim();
const idx = el.dataset.index || ;
if (!src || weaponVideosCache.has(idx)) return;
weaponTotalVideos++;
const v = document.createElement('video');
v.className = 'skill-video';
v.setAttribute('controls', );
v.setAttribute('preload', 'auto');
v.setAttribute('playsinline', );
v.style.display = 'none';
v.dataset.index = idx;
v.style.width = '100%';
v.style.maxWidth = '100%';
v.style.height = 'auto';
v.style.aspectRatio = '16/9';
v.style.objectFit = 'cover';
const source = document.createElement('source');
source.src = src;
source.type = 'video/webm';
v.appendChild(source);
v.addEventListener('canplay', () => {
weaponLoadedVideos++;
if (weaponLoadedVideos === weaponTotalVideos) weaponAutoplay = true;
});
v.addEventListener('error', () => {
weaponLoadedVideos++;
if (weaponLoadedVideos === weaponTotalVideos) weaponAutoplay = true;
});
weaponVideoBox.appendChild(v);
weaponVideosCache.set(idx, v);
});
// Wire clicks for weapon icons
weaponIconItems.forEach(el => {
if (el.dataset.wired) return;
el.dataset.wired = '1';
const label = el.dataset.nome || el.dataset.name || ;
el.setAttribute('aria-label', label);
if (el.hasAttribute('title')) el.removeAttribute('title');
const img = el.querySelector('img');
if (img) {
img.setAttribute('alt', );
if (img.hasAttribute('title')) img.removeAttribute('title');
}
el.addEventListener('click', () => {
const skillsRoot = document.getElementById('skills');
const i18nMap = skillsRoot ? JSON.parse(skillsRoot.dataset.i18nAttrs || '{}') : {};
const lang = getLangKey();
const L = 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'
};
const name = el.dataset.nome || el.dataset.name || ;
const level = (el.dataset.level || ).trim();
const descPack = {
pt: el.dataset.descPt || ,
en: el.dataset.descEn || ,
es: el.dataset.descEs || ,
pl: el.dataset.descPl ||
};
const chosenDesc = descPack[lang] || descPack.pt || descPack.en || descPack.es || descPack.pl || el.dataset.desc || ;
const descHtml = chosenDesc.replace(/(.*?)/g, '$1');
const attrsHTML = el.dataset.atr ? renderAttributes(el.dataset.atr) : ;
if (weaponDescBox) {
weaponDescBox.innerHTML = `
${name}
${level ? `
${L.level} ${level}
` : }
${attrsHTML}
${descHtml}
`;
}
weaponIconItems.forEach(i => i.classList.remove('active'));
el.classList.add('active');
if (!weaponAutoplay && weaponLoadedVideos > 0) weaponAutoplay = true;
window.__lastActiveSkillIcon = el;
// Show video
if (weaponVideoBox) {
Array.from(weaponVideoBox.querySelectorAll('video.skill-video')).forEach(v => {
try { v.pause(); } catch (e) { }
v.style.display = 'none';
});
}
const hasIdx = !!el.dataset.index;
const hasVideo = !!(el.dataset.video && el.dataset.video.trim() !== );
if (!weaponVideoBox || !hasVideo) {
if (weaponVideoBox) weaponVideoBox.style.display = 'none';
return;
}
if (hasIdx && weaponVideosCache.has(el.dataset.index)) {
const v = weaponVideosCache.get(el.dataset.index);
weaponVideoBox.style.display = 'block';
v.style.display = 'block';
try { v.currentTime = 0; } catch (e) { }
const suppress = document.body.dataset.suppressSkillPlay === '1';
if (!suppress && weaponAutoplay) v.play().catch(() => { });
}
});
});
// Wire tooltips for weapon icons
const tip = document.querySelector('.skill-tooltip');
if (tip) {
weaponIconItems.forEach(icon => {
if (icon.dataset.tipwired) return;
icon.dataset.tipwired = '1';
const label = icon.dataset.nome || icon.dataset.name || icon.title || ;
if (label && !icon.hasAttribute('aria-label')) icon.setAttribute('aria-label', label);
if (icon.hasAttribute('title')) icon.removeAttribute('title');
const measureAndPos = (el) => {
if (!el || tip.getAttribute('aria-hidden') === 'true') return;
tip.style.left = '0px';
tip.style.top = '0px';
const rect = el.getBoundingClientRect();
const tr = tip.getBoundingClientRect();
let left = Math.round(rect.left + (rect.width - tr.width) / 2);
left = Math.max(8, Math.min(left, window.innerWidth - tr.width - 8));
const coarse = (window.matchMedia && matchMedia('(pointer: coarse)').matches) || (window.innerWidth <= 600);
let top = coarse ? Math.round(rect.bottom + 10) : Math.round(rect.top - tr.height - 8);
if (top < 8) top = Math.round(rect.bottom + 10);
tip.style.left = left + 'px';
tip.style.top = top + 'px';
};
const show = (el, text) => {
tip.textContent = text || ;
tip.setAttribute('aria-hidden', 'false');
measureAndPos(el);
tip.style.opacity = '1';
};
const hide = () => {
tip.setAttribute('aria-hidden', 'true');
tip.style.opacity = '0';
tip.style.left = '-9999px';
tip.style.top = '-9999px';
};
icon.addEventListener('mouseenter', () => show(icon, label));
icon.addEventListener('mousemove', () => measureAndPos(icon));
icon.addEventListener('click', () => { window.__globalSkillTooltip.lockUntil.value = performance.now() + 240; measureAndPos(icon); });
icon.addEventListener('mouseleave', hide);
});
}
// Do not pre-mark active here; activation handled on tab enter
})();
// Debug logging
setTimeout(() => {
Array.from(document.querySelectorAll('.skill-icon')).forEach(el => {
console.log('icon', el.dataset.index, 'data-video=', el.dataset.video);
});
videosCache.forEach((v, idx) => {
const src = v.querySelector('source') ? v.querySelector('source').src : v.src;
console.log('video element', idx, 'src=', src, 'readyState=', v.readyState);
v.addEventListener('error', (ev) => {
console.error('VIDEO ERROR idx=', idx, 'src=', src, 'error=', v.error);
});
v.addEventListener('loadedmetadata', () => {
console.log('loadedmetadata idx=', idx, 'dimensions=', v.videoWidth, 'x', v.videoHeight);
});
});
}, 600);
})();
</script>