Mudanças entre as edições de "Widget:Character.Subskills"

De Wiki Gla
Ir para navegação Ir para pesquisar
(flag no canto do video)
m
Linha 1: Linha 1:
<!-- ===========================
<!-- SUBSKILLS SYSTEM -->
    SUBSKILLS SYSTEM
    =========================== -->
<script>
<script>
     (function () {
     (function () {
        // ===========================
        // Subskills API & Core Functions
        // ===========================
         const api = (window.__subskills ||= {});
         const api = (window.__subskills ||= {});
         const subCache = new Map();
         const subCache = new Map();
         const imagePreloadCache = new Map(); // Cache para pré-carregar imagens
         const imagePreloadCache = new Map();
         let subRail, subBar, spacer;
         let subRail, subBar, spacer;


         // Utility functions
         // ===== HERANÇA DE ATRIBUTOS: busca dados das skills principais =====
        function getMainSkillsMap() {
            const map = new Map();
            const icons = document.querySelectorAll('.icon-bar .skill-icon[data-nome]');
            icons.forEach(icon => {
                const name = (icon.dataset.nome || '').trim();
                if (!name) return;
 
                // Extrai atributos do data-atr (formato: "key:value, key:value")
                const atrRaw = icon.dataset.atr || '';
                const attrs = {};
                atrRaw.split(',').forEach(pair => {
                    const [k, v] = pair.split(':').map(x => (x || '').trim());
                    if (k && v) attrs[k] = v;
                });
 
                map.set(name, {
                    icon: icon.querySelector('img')?.src?.match(/FilePath\/(.+)$/)?.[1] || icon.dataset.icon || '',
                    level: icon.dataset.level || '',
                    video: icon.dataset.video || '',
                    powerpve: attrs.powerpve || attrs.power || '',
                    powerpvp: attrs.powerpvp || attrs['power pvp'] || '',
                    cooldown: attrs.cooldown || attrs.recarga || '',
                    energy: attrs.energy || attrs.energia || '',
                    descPt: icon.dataset.descPt || '',
                    descEn: icon.dataset.descEn || '',
                    descEs: icon.dataset.descEs || '',
                    descPl: icon.dataset.descPl || ''
                });
            });
            return map;
        }
 
        // Aplica herança: se atributo da subskill estiver vazio, usa da skill principal
        function applyInheritance(sub, mainSkillsMap) {
            const name = (sub.name || sub.n || '').trim();
            const main = mainSkillsMap.get(name);
            if (!main) return sub;
 
            return {
                ...sub,
                icon: sub.icon && sub.icon !== 'Nada.png' ? sub.icon : main.icon || sub.icon,
                level: sub.level || main.level,
                video: sub.video || main.video,
                powerpve: sub.powerpve || main.powerpve,
                powerpvp: sub.powerpvp || main.powerpvp,
                cooldown: sub.cooldown || main.cooldown,
                energy: sub.energy || main.energy,
                descPt: sub.descPt || (sub.desc_i18n?.pt) || main.descPt,
                descEn: sub.descEn || (sub.desc_i18n?.en) || main.descEn,
                descEs: sub.descEs || (sub.desc_i18n?.es) || main.descEs,
                descPl: sub.descPl || (sub.desc_i18n?.pl) || main.descPl
            };
        }
 
         function filePathURL(fileName) {
         function filePathURL(fileName) {
             const f = encodeURIComponent((fileName || 'Nada.png').replace(/^Arquivo:|^File:/, ''));
             const f = encodeURIComponent((fileName || 'Nada.png').replace(/^Arquivo:|^File:/, ''));
Linha 21: Linha 70:
         }
         }


        // Pré-carrega uma imagem (retorna Promise)
         function preloadImage(iconFile) {
         function preloadImage(iconFile) {
             const url = filePathURL(iconFile || 'Nada.png');
             const url = filePathURL(iconFile || 'Nada.png');
Linha 30: Linha 78:
                 const img = new Image();
                 const img = new Image();
                 img.onload = () => resolve(url);
                 img.onload = () => resolve(url);
                 img.onerror = () => resolve(url); // resolve mesmo com erro para não travar
                 img.onerror = () => resolve(url);
                 img.src = url;
                 img.src = url;
             });
             });
Linha 43: Linha 91:
             const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
             const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
             return i18nMap[lang] || i18nMap.pt || {
             return i18nMap[lang] || i18nMap.pt || {
                 cooldown: 'Recarga', energy_gain: 'Ganho de energia', energy_cost: 'Custo de energia',
                 cooldown: 'Recarga',
                 power: 'Poder', power_pvp: 'Poder PvP', level: 'Nível'
                energy_gain: 'Ganho de energia',
                energy_cost: 'Custo de energia',
                 power: 'Poder',
                power_pvp: 'Poder PvP',
                level: 'Nível'
             };
             };
         }
         }
Linha 62: Linha 114:
             return rows.length ? `<div class="attr-list">${rows.join('')}</div>` : '';
             return rows.length ? `<div class="attr-list">${rows.join('')}</div>` : '';
         }
         }
         function renderFlagsRow(flags) {
         function renderFlagsRow(flags) {
             const map = {
             const map = {
Linha 74: Linha 127:
             return `<div class="skill-flags" role="group" aria-label="Características">${items}</div>`;
             return `<div class="skill-flags" role="group" aria-label="Características">${items}</div>`;
         }
         }
         function applyFlagTooltips(container) {
         function applyFlagTooltips(container) {
             const skillsRoot = document.getElementById('skills');
             const skillsRoot = document.getElementById('skills');
             if (!skillsRoot) return;
             if (!skillsRoot) return;
             let pack = {};
             let pack = {};
             try {
             try { pack = JSON.parse(skillsRoot.dataset.i18nFlags || '{}'); } catch (e) { }
                pack = JSON.parse(skillsRoot.dataset.i18nFlags || '{}');
            } catch (e) { }
             const raw = (document.documentElement.lang || 'pt').toLowerCase();
             const raw = (document.documentElement.lang || 'pt').toLowerCase();
             const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
             const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
Linha 86: Linha 138:
             const flags = container.querySelectorAll('.skill-flags .skill-flag[data-flag]');
             const flags = container.querySelectorAll('.skill-flags .skill-flag[data-flag]');
             const tooltip = window.__globalSkillTooltip;
             const tooltip = window.__globalSkillTooltip;
             if (!tooltip) return; // Tooltip system not ready yet
             if (!tooltip) return;


             flags.forEach(el => {
             flags.forEach(el => {
Linha 92: Linha 144:
                 const tip = (dict && dict[key]) || '';
                 const tip = (dict && dict[key]) || '';
                 if (!tip) return;
                 if (!tip) return;
                // Remove old listeners if any
                 if (el.dataset.flagTipWired) return;
                 if (el.dataset.flagTipWired) return;
                 el.dataset.flagTipWired = '1';
                 el.dataset.flagTipWired = '1';
                // Set aria-label for accessibility
                 el.setAttribute('aria-label', tip);
                 el.setAttribute('aria-label', tip);
                 if (el.hasAttribute('title')) el.removeAttribute('title');
                 if (el.hasAttribute('title')) el.removeAttribute('title');


                // Wire tooltip events using the same system as skill icons
                 el.addEventListener('mouseenter', () => {
                 el.addEventListener('mouseenter', () => {
                     const tipEl = document.querySelector('.skill-tooltip');
                     const tipEl = document.querySelector('.skill-tooltip');
Linha 124: Linha 171:
         }
         }


        // ===========================
        // Subskills UI Management
        // ===========================
         function ensureRail(iconsBar) {
         function ensureRail(iconsBar) {
             const rail = iconsBar.closest('.top-rail');
             const rail = iconsBar.closest('.top-rail');
             if (!rail) return null;
             if (!rail) return null;
             if (!subRail) {
             if (!subRail) {
                 subRail = document.createElement('div');
                 subRail = document.createElement('div');
Linha 151: Linha 194:
         function ensureSubVideoCached(s, parentIdx, videoBox) {
         function ensureSubVideoCached(s, parentIdx, videoBox) {
             const key = `sub:${parentIdx}:${(s.name || s.n || '').trim()}`;
             const key = `sub:${parentIdx}:${(s.name || s.n || '').trim()}`;
            // Verificar se já foi pré-criado pelo sistema de preload global
             if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) {
             if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) {
                 const precreated = window.__subskillVideosCache.get(key);
                 const precreated = window.__subskillVideosCache.get(key);
Linha 160: Linha 201:
                 return key;
                 return key;
             }
             }
             if (subCache.has(key)) return key;
             if (subCache.has(key)) return key;
             if (!s.video) return key;
             if (!s.video) return key;
Linha 176: Linha 216:
             src.type = 'video/webm';
             src.type = 'video/webm';
             v.appendChild(src);
             v.appendChild(src);
             videoBox.appendChild(v);
             videoBox.appendChild(v);
             subCache.set(key, v);
             subCache.set(key, v);
            // Forçar carregamento
             v.load();
             v.load();
             return key;
             return key;
         }
         }


         function showSubVideo(key, videoBox) {
         function showSubVideo(key, videoBox) {
             videoBox.querySelectorAll('.skill-video').forEach(v => { try { v.pause() } catch { }; v.style.display = 'none'; });
             videoBox.querySelectorAll('.skill-video').forEach(v => {
 
                try { v.pause(); } catch { }
            // Tentar obter do cache global primeiro, depois do cache local
                v.style.display = 'none';
            });
             let v = null;
             let v = null;
             if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) {
             if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) {
Linha 197: Linha 234:
                 v = subCache.get(key);
                 v = subCache.get(key);
             }
             }
 
             if (!v) {
             if (!v) { videoBox.style.display = 'none'; return; }
                videoBox.style.display = 'none';
                return;
            }
             videoBox.style.display = 'block';
             videoBox.style.display = 'block';
             v.style.display = 'block';
             v.style.display = 'block';
             try { v.currentTime = 0; } catch { }
             try { v.currentTime = 0; } catch { }
             const suppress = document.body.dataset.suppressSkillPlay === '1';
             const suppress = document.body.dataset.suppressSkillPlay === '1';
             if (!suppress) {
             if (!suppress) {
Linha 209: Linha 247:
         }
         }


        // ===========================
        // Public API
        // ===========================
         api.refreshCurrentSubSafe = function () {
         api.refreshCurrentSubSafe = function () {
             const btn = document.querySelector('.subskills-rail .subicon.active');
             const btn = document.querySelector('.subskills-rail .subicon.active');
             if (!btn) return false;
             if (!btn) return false;
             const had = document.body.dataset.suppressSkillPlay;
             const had = document.body.dataset.suppressSkillPlay;
             document.body.dataset.suppressSkillPlay = '1';
             document.body.dataset.suppressSkillPlay = '1';
Linha 243: Linha 277:
             }
             }


             let subs; try { subs = JSON.parse(rawSubs); } catch { subs = []; }
             let subs;
            try { subs = JSON.parse(rawSubs); } catch { subs = []; }
             if (!Array.isArray(subs) || subs.length === 0) {
             if (!Array.isArray(subs) || subs.length === 0) {
                 subRail.classList.add('collapsed');
                 subRail.classList.add('collapsed');
Linha 252: Linha 287:
             }
             }


             // monta tudo off-screen
             // Busca mapa das skills principais para herança
            const mainSkillsMap = getMainSkillsMap();
 
             subRail.classList.add('hidden');
             subRail.classList.add('hidden');
             subBar.innerHTML = '';
             subBar.innerHTML = '';
Linha 267: Linha 304:
             }
             }


            // pré-carrega vídeos e imagens
             order.forEach(nm => {
             order.forEach(nm => {
                 const s = subs.find(x => (x.name || x.n || '') === nm);
                 let s = subs.find(x => (x.name || x.n || '') === nm);
                 if (s) {
                 if (s) {
                    // Aplica herança de atributos
                    s = applyInheritance(s, mainSkillsMap);
                     if (s.video) ensureSubVideoCached(s, parentIdx, videoBox);
                     if (s.video) ensureSubVideoCached(s, parentIdx, videoBox);
                     if (s.icon) preloadImage(s.icon);
                     if (s.icon) preloadImage(s.icon);
Linha 277: Linha 315:


             order.forEach(nm => {
             order.forEach(nm => {
                 const s = subs.find(x => (x.name || x.n || '') === nm); if (!s) return;
                 let s = subs.find(x => (x.name || x.n || '') === nm);
                if (!s) return;
                // Aplica herança de atributos
                s = applyInheritance(s, mainSkillsMap);
 
                 const item = document.createElement('div');
                 const item = document.createElement('div');
                 item.className = 'subicon';
                 item.className = 'subicon';
                 item.title = s.name || nm;
                 item.title = s.name || nm;
                // Store slug for hash matching
 
                 const slugify = window.__skillSlugify || ((str) => (str || '').toLowerCase().replace(/[^\w]+/g, '-').replace(/^-+|-+$/g, ''));
                 const slugify = window.__skillSlugify || ((str) => (str || '').toLowerCase().replace(/[^\w]+/g, '-').replace(/^-+|-+$/g, ''));
                 item.dataset.slug = slugify(s.name || nm);
                 item.dataset.slug = slugify(s.name || nm);
Linha 292: Linha 334:
                 item.addEventListener('click', () => {
                 item.addEventListener('click', () => {
                     const L = getLabels();
                     const L = getLabels();
                     const descI18n = {
                     const descI18n = {
                         pt: s.descPt || (s.desc_i18n && s.desc_i18n.pt) || '',
                         pt: s.descPt || (s.desc_i18n && s.desc_i18n.pt) || '',
Linha 309: Linha 350:
                             flagsHTML = renderFlagsRow(s.flags);
                             flagsHTML = renderFlagsRow(s.flags);
                         }
                         }
                         descBox.innerHTML = `
                         descBox.innerHTML = `<div class="skill-title"><h3>${s.name || nm}</h3></div>${level ? `<div class="skill-level-line"><span class="attr-label">${L.level} ${level}</span></div>` : ''}${renderSubAttrs(s, L)}<div class="desc">${chosen.replace(/'''(.*?)'''/g, '<b>$1</b>')}</div>`;
            <div class="skill-title"><h3>${s.name || nm}</h3></div>
            ${level ? `<div class="skill-level-line"><span class="attr-label">${L.level} ${level}</span></div>` : ''}
            ${renderSubAttrs(s, L)}
            <div class="desc">${chosen.replace(/'''(.*?)'''/g, '<b>$1</b>')}</div>
          `;
                     }
                     }
                    // Renderizar flags dentro do videoBox (canto superior esquerdo do vídeo)
 
                     if (videoBox) {
                     if (videoBox) {
                        // Remover flags antigas
                         const oldFlags = videoBox.querySelector('.skill-flags');
                         const oldFlags = videoBox.querySelector('.skill-flags');
                         if (oldFlags) oldFlags.remove();
                         if (oldFlags) oldFlags.remove();
                         if (flagsHTML) {
                         if (flagsHTML) {
                             videoBox.insertAdjacentHTML('beforeend', flagsHTML);
                             videoBox.insertAdjacentHTML('beforeend', flagsHTML);
Linha 330: Linha 364:
                     const key = `sub:${parentIdx}:${(s.name || s.n || '').trim()}`;
                     const key = `sub:${parentIdx}:${(s.name || s.n || '').trim()}`;
                     showSubVideo(key, videoBox);
                     showSubVideo(key, videoBox);
                     Array.from(subBar.children).forEach(c => c.classList.remove('active'));
                     Array.from(subBar.children).forEach(c => c.classList.remove('active'));
                     item.classList.add('active');
                     item.classList.add('active');
                    // Track for language changes
                     window.__lastActiveSkillIcon = item;
                     window.__lastActiveSkillIcon = item;
                 });
                 });


                // Tooltip wiring: use global tooltip system (consistent with main skills)
                 if (window.__globalSkillTooltip) {
                 if (window.__globalSkillTooltip) {
                     const { show, hide, measureAndPos, lockUntil } = window.__globalSkillTooltip;
                     const { show, hide, measureAndPos, lockUntil } = window.__globalSkillTooltip;
Linha 353: Linha 383:
             });
             });


            // abre só depois de construir tudo (evita "toquinho")
             requestAnimationFrame(() => {
             requestAnimationFrame(() => {
                 subRail.classList.remove('collapsed');
                 subRail.classList.remove('collapsed');
Linha 369: Linha 398:
         };
         };


        // alias antigo pra manter compatibilidade
         window.renderSubskillsBarFrom = function (el, ctx) { api.renderBarFrom(el, ctx); };
         window.renderSubskillsBarFrom = function (el, ctx) { api.renderBarFrom(el, ctx); };


        // ===========================
        // Pré-carregamento global de imagens de subskills ao carregar a página
        // ===========================
         api.preloadAllSubskillImages = function () {
         api.preloadAllSubskillImages = function () {
             const allSkillIcons = document.querySelectorAll('.icon-bar .skill-icon[data-subs]');
             const allSkillIcons = document.querySelectorAll('.icon-bar .skill-icon[data-subs]');
Linha 392: Linha 417:
                             totalImages++;
                             totalImages++;
                         }
                         }
                        // Pré-carrega também subskills aninhadas (sub de sub)
                         if (s && Array.isArray(s.subs)) {
                         if (s && Array.isArray(s.subs)) {
                             s.subs.forEach(nested => {
                             s.subs.forEach(nested => {
Linha 416: Linha 440:
         };
         };


        // Executa pré-carregamento quando o DOM estiver pronto
         if (document.readyState === 'loading') {
         if (document.readyState === 'loading') {
             document.addEventListener('DOMContentLoaded', () => {
             document.addEventListener('DOMContentLoaded', () => {
Linha 426: Linha 449:
     })();
     })();
</script>
</script>
<style>
<style>
    /* ===========================
      SUBSKILLS BAR
      =========================== */
     .subicon-bar {
     .subicon-bar {
         display: flex;
         display: flex;
Linha 436: Linha 455:
         padding: 6px 6px;
         padding: 6px 6px;
         overflow-x: auto;
         overflow-x: auto;
        /* Firefox */
         scrollbar-width: thin;
         scrollbar-width: thin;
         scrollbar-color: #ababab transparent;
         scrollbar-color: #ababab transparent;
Linha 498: Linha 518:
             0 0 0 calc(var(--icon-ring-w, 2px) * 2) var(--icon-active-ring, rgba(255, 217, 90, .50));
             0 0 0 calc(var(--icon-ring-w, 2px) * 2) var(--icon-active-ring, rgba(255, 217, 90, .50));
     }
     }
</style>
 
<!-- ===========================
    SUBSKILLS RAIL SYSTEM
    =========================== -->
<style>
    /* ===========================
      SUBSKILLS RAIL LAYOUT
      =========================== */
     .top-rail.skills {
     .top-rail.skills {
         position: relative;
         position: relative;
Linha 570: Linha 583:
     }
     }


    /* ===========================
      SUBSKILLS RAIL ICONS
      =========================== */
     .subskills-rail .subicon-bar {
     .subskills-rail .subicon-bar {
         display: inline-flex;
         display: inline-flex;
Linha 578: Linha 588:
         gap: 0;
         gap: 0;
         overflow-x: auto;
         overflow-x: auto;
        /* Firefox */
         scrollbar-width: thin;
         scrollbar-width: thin;
         scrollbar-color: #ababab transparent;
         scrollbar-color: #ababab transparent;
Linha 600: Linha 611:
         cursor: pointer;
         cursor: pointer;
         isolation: isolate;
         isolation: isolate;
        -webkit-backface-visibility: hidden;
         backface-visibility: hidden;
         backface-visibility: hidden;
         transform: translateZ(0);
         transform: translateZ(0);
Linha 635: Linha 647:
     }
     }


    /* ===========================
      SUBSKILLS VIDEO STYLES
      =========================== */
     .video-container .skill-video {
     .video-container .skill-video {
         width: 100%;
         width: 100%;
Linha 647: Linha 656:
     }
     }


    /* ===========================
      SUBSKILLS RESPONSIVE
      =========================== */
     @media (max-width: 900px) {
     @media (max-width: 900px) {
         .subskills-rail {
         .subskills-rail {
Linha 664: Linha 670:
     }
     }


    /* Container to layer back handle behind rail */
     .skills-rail-wrap {
     .skills-rail-wrap {
         position: relative;
         position: relative;
Linha 670: Linha 675:
         width: max-content;
         width: max-content;
         margin: 0 auto;
         margin: 0 auto;
        /* center like original top-rail */
     }
     }
    /* Removed duplicate CSS - all rules now in main .skills-back-wrapper above */
</style>
</style>

Edição das 03h39min de 30 de novembro de 2025

<script>

   (function () {
       const api = (window.__subskills ||= {});
       const subCache = new Map();
       const imagePreloadCache = new Map();
       let subRail, subBar, spacer;
       // ===== HERANÇA DE ATRIBUTOS: busca dados das skills principais =====
       function getMainSkillsMap() {
           const map = new Map();
           const icons = document.querySelectorAll('.icon-bar .skill-icon[data-nome]');
           icons.forEach(icon => {
               const name = (icon.dataset.nome || ).trim();
               if (!name) return;
               // Extrai atributos do data-atr (formato: "key:value, key:value")
               const atrRaw = icon.dataset.atr || ;
               const attrs = {};
               atrRaw.split(',').forEach(pair => {
                   const [k, v] = pair.split(':').map(x => (x || ).trim());
                   if (k && v) attrs[k] = v;
               });
               map.set(name, {
                   icon: icon.querySelector('img')?.src?.match(/FilePath\/(.+)$/)?.[1] || icon.dataset.icon || ,
                   level: icon.dataset.level || ,
                   video: icon.dataset.video || ,
                   powerpve: attrs.powerpve || attrs.power || ,
                   powerpvp: attrs.powerpvp || attrs['power pvp'] || ,
                   cooldown: attrs.cooldown || attrs.recarga || ,
                   energy: attrs.energy || attrs.energia || ,
                   descPt: icon.dataset.descPt || ,
                   descEn: icon.dataset.descEn || ,
                   descEs: icon.dataset.descEs || ,
                   descPl: icon.dataset.descPl || 
               });
           });
           return map;
       }
       // Aplica herança: se atributo da subskill estiver vazio, usa da skill principal
       function applyInheritance(sub, mainSkillsMap) {
           const name = (sub.name || sub.n || ).trim();
           const main = mainSkillsMap.get(name);
           if (!main) return sub;
           return {
               ...sub,
               icon: sub.icon && sub.icon !== 'Nada.png' ? sub.icon : main.icon || sub.icon,
               level: sub.level || main.level,
               video: sub.video || main.video,
               powerpve: sub.powerpve || main.powerpve,
               powerpvp: sub.powerpvp || main.powerpvp,
               cooldown: sub.cooldown || main.cooldown,
               energy: sub.energy || main.energy,
               descPt: sub.descPt || (sub.desc_i18n?.pt) || main.descPt,
               descEn: sub.descEn || (sub.desc_i18n?.en) || main.descEn,
               descEs: sub.descEs || (sub.desc_i18n?.es) || main.descEs,
               descPl: sub.descPl || (sub.desc_i18n?.pl) || main.descPl
           };
       }
       function filePathURL(fileName) {
           const f = encodeURIComponent((fileName || 'Nada.png').replace(/^Arquivo:|^File:/, ));
           const base = (window.mw && mw.util && typeof mw.util.wikiScript === 'function')
               ? mw.util.wikiScript()
               : (window.mw && mw.config ? (mw.config.get('wgScript') || '/index.php') : '/index.php');
           return `${base}?title=Especial:FilePath/${f}`;
       }
       function preloadImage(iconFile) {
           const url = filePathURL(iconFile || 'Nada.png');
           if (imagePreloadCache.has(url)) {
               return imagePreloadCache.get(url);
           }
           const promise = new Promise((resolve, reject) => {
               const img = new Image();
               img.onload = () => resolve(url);
               img.onerror = () => resolve(url);
               img.src = url;
           });
           imagePreloadCache.set(url, promise);
           return promise;
       }
       function getLabels() {
           const skillsRoot = document.getElementById('skills');
           const i18nMap = skillsRoot ? JSON.parse(skillsRoot.dataset.i18nAttrs || '{}') : {};
           const raw = (document.documentElement.lang || skillsRoot?.dataset.i18nDefault || 'pt').toLowerCase();
           const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
           return i18nMap[lang] || i18nMap.pt || {
               cooldown: 'Recarga',
               energy_gain: 'Ganho de energia',
               energy_cost: 'Custo de energia',
               power: 'Poder',
               power_pvp: 'Poder PvP',
               level: 'Nível'
           };
       }
       function renderSubAttrs(s, L) {

const chip = (label, val) => (val ? `

${label}${val}

` : );

           const pve = (s.powerpve || ).toString().trim();
           const pvp = (s.powerpvp || ).toString().trim();
           const en = (s.energy || ).toString().trim();
           const cd = (s.cooldown || ).toString().trim();
           const rows = [
               cd ? chip(L.cooldown, cd) : ,
               en ? chip((en.startsWith('-') ? L.energy_cost : L.energy_gain), en.startsWith('-') ? en.replace(/^-/, ) : en.replace(/^\+?/, )) : ,
               pve ? chip(L.power, pve) : ,
               pvp ? chip(L.power_pvp, pvp) : ,
           ].filter(Boolean);

return rows.length ? `

${rows.join()}

` : ;

       }
       function renderFlagsRow(flags) {
           const map = {
               aggro: 'Enemyaggro-icon.png',
               bridge: 'Bridgemaker-icon.png',
               wall: 'Destroywall-icon.png',
               quickcast: 'Quickcast-icon.png'
           };
           const arr = (flags || []).filter(Boolean);
           if (!arr.length) return ;
           const items = arr.map(k => `<img class="skill-flag" data-flag="${k}" alt="" src="${filePathURL(map[k])}">`).join();

return `

${items}

`;

       }
       function applyFlagTooltips(container) {
           const skillsRoot = document.getElementById('skills');
           if (!skillsRoot) return;
           let pack = {};
           try { pack = JSON.parse(skillsRoot.dataset.i18nFlags || '{}'); } catch (e) { }
           const raw = (document.documentElement.lang || 'pt').toLowerCase();
           const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
           const dict = pack[lang] || pack.pt || {};
           const flags = container.querySelectorAll('.skill-flags .skill-flag[data-flag]');
           const tooltip = window.__globalSkillTooltip;
           if (!tooltip) return;
           flags.forEach(el => {
               const key = el.getAttribute('data-flag');
               const tip = (dict && dict[key]) || ;
               if (!tip) return;
               if (el.dataset.flagTipWired) return;
               el.dataset.flagTipWired = '1';
               el.setAttribute('aria-label', tip);
               if (el.hasAttribute('title')) el.removeAttribute('title');
               el.addEventListener('mouseenter', () => {
                   const tipEl = document.querySelector('.skill-tooltip');
                   if (tipEl) tipEl.classList.add('flag-tooltip');
                   tooltip.show(el, tip);
               });
               el.addEventListener('mousemove', () => {
                   if (performance.now() >= tooltip.lockUntil.value) {
                       tooltip.measureAndPos(el);
                   }
               });
               el.addEventListener('click', () => {
                   tooltip.lockUntil.value = performance.now() + 240;
                   tooltip.measureAndPos(el);
               });
               el.addEventListener('mouseleave', () => {
                   const tipEl = document.querySelector('.skill-tooltip');
                   if (tipEl) tipEl.classList.remove('flag-tooltip');
                   tooltip.hide();
               });
           });
       }
       function ensureRail(iconsBar) {
           const rail = iconsBar.closest('.top-rail');
           if (!rail) return null;
           if (!subRail) {
               subRail = document.createElement('div');
               subRail.className = 'subskills-rail collapsed hidden';
               rail.appendChild(subRail);
           }
           if (!subBar) {
               subBar = document.createElement('div');
               subBar.className = 'subicon-bar';
               subRail.appendChild(subBar);
           }
           if (!spacer) {
               spacer = document.createElement('div');
               spacer.className = 'subskills-spacer';
               rail.parentNode.insertBefore(spacer, rail.nextSibling);
           }
           return rail;
       }
       function ensureSubVideoCached(s, parentIdx, videoBox) {
           const key = `sub:${parentIdx}:${(s.name || s.n || ).trim()}`;
           if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) {
               const precreated = window.__subskillVideosCache.get(key);
               if (!subCache.has(key)) {
                   subCache.set(key, precreated);
               }
               return key;
           }
           if (subCache.has(key)) return key;
           if (!s.video) return key;
           const v = document.createElement('video');
           v.className = 'skill-video';
           v.dataset.sub = '1';
           v.setAttribute('controls', );
           v.setAttribute('preload', 'auto');
           v.setAttribute('playsinline', );
           Object.assign(v.style, { display: 'none', width: '100%', height: 'auto', aspectRatio: '16/9', objectFit: 'cover' });
           const src = document.createElement('source');
           src.src = filePathURL(s.video);
           src.type = 'video/webm';
           v.appendChild(src);
           videoBox.appendChild(v);
           subCache.set(key, v);
           v.load();
           return key;
       }
       function showSubVideo(key, videoBox) {
           videoBox.querySelectorAll('.skill-video').forEach(v => {
               try { v.pause(); } catch { }
               v.style.display = 'none';
           });
           let v = null;
           if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) {
               v = window.__subskillVideosCache.get(key);
           }
           if (!v) {
               v = subCache.get(key);
           }
           if (!v) {
               videoBox.style.display = 'none';
               return;
           }
           videoBox.style.display = 'block';
           v.style.display = 'block';
           try { v.currentTime = 0; } catch { }
           const suppress = document.body.dataset.suppressSkillPlay === '1';
           if (!suppress) {
               v.play?.().catch(() => { });
           }
       }
       api.refreshCurrentSubSafe = function () {
           const btn = document.querySelector('.subskills-rail .subicon.active');
           if (!btn) return false;
           const had = document.body.dataset.suppressSkillPlay;
           document.body.dataset.suppressSkillPlay = '1';
           try {
               btn.dispatchEvent(new Event('click', { bubbles: true }));
           } finally {
               if (had) document.body.dataset.suppressSkillPlay = had;
               else delete document.body.dataset.suppressSkillPlay;
           }
           return true;
       };
       api.renderBarFrom = function (el, { iconsBar, descBox, videoBox }) {
           const rail = ensureRail(iconsBar);
           if (!rail) return;
           const rawSubs = el.getAttribute('data-subs') || ;
           const rawOrder = el.getAttribute('data-suborder') || ;
           const parentIdx = el.dataset.index || ;
           if (!rawSubs.trim()) {
               subRail.classList.add('collapsed');
               subRail.classList.add('hidden');
               subBar.innerHTML = ;
               if (spacer) spacer.style.height = '0px';
               return;
           }
           let subs;
           try { subs = JSON.parse(rawSubs); } catch { subs = []; }
           if (!Array.isArray(subs) || subs.length === 0) {
               subRail.classList.add('collapsed');
               subRail.classList.add('hidden');
               subBar.innerHTML = ;
               if (spacer) spacer.style.height = '0px';
               return;
           }
           // Busca mapa das skills principais para herança
           const mainSkillsMap = getMainSkillsMap();
           subRail.classList.add('hidden');
           subBar.innerHTML = ;
           let order = subs.map(s => s.name || s.n || );
           if (rawOrder.trim()) {
               try {
                   const preferred = JSON.parse(rawOrder);
                   if (Array.isArray(preferred) && preferred.length) {
                       const byName = new Map(subs.map(s => [(s.name || s.n || ), s]));
                       order = preferred.filter(n => byName.has(n));
                   }
               } catch { }
           }
           order.forEach(nm => {
               let s = subs.find(x => (x.name || x.n || ) === nm);
               if (s) {
                   // Aplica herança de atributos
                   s = applyInheritance(s, mainSkillsMap);
                   if (s.video) ensureSubVideoCached(s, parentIdx, videoBox);
                   if (s.icon) preloadImage(s.icon);
               }
           });
           order.forEach(nm => {
               let s = subs.find(x => (x.name || x.n || ) === nm);
               if (!s) return;
               // Aplica herança de atributos
               s = applyInheritance(s, mainSkillsMap);
               const item = document.createElement('div');
               item.className = 'subicon';
               item.title = s.name || nm;
               const slugify = window.__skillSlugify || ((str) => (str || ).toLowerCase().replace(/[^\w]+/g, '-').replace(/^-+|-+$/g, ));
               item.dataset.slug = slugify(s.name || nm);
               const img = document.createElement('img');
               img.alt = ;
               img.src = filePathURL(s.icon || 'Nada.png');
               item.appendChild(img);
               item.addEventListener('click', () => {
                   const L = getLabels();
                   const descI18n = {
                       pt: s.descPt || (s.desc_i18n && s.desc_i18n.pt) || ,
                       en: s.descEn || (s.desc_i18n && s.desc_i18n.en) || ,
                       es: s.descEs || (s.desc_i18n && s.desc_i18n.es) || ,
                       pl: s.descPl || (s.desc_i18n && s.desc_i18n.pl) || 
                   };
                   const raw = (document.documentElement.lang || 'pt').toLowerCase();
                   const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
                   const chosen = descI18n[lang] || descI18n.pt || ;
                   if (descBox) {
                       const level = (s.level || ).toString().trim();
                       let flagsHTML = ;
                       if (Array.isArray(s.flags) && s.flags.length > 0) {
                           flagsHTML = renderFlagsRow(s.flags);
                       }

descBox.innerHTML = `

${s.name || nm}

${level ? `

${L.level} ${level}

` : }${renderSubAttrs(s, L)}

${chosen.replace(/(.*?)/g, '$1')}

`;

                   }
                   if (videoBox) {
                       const oldFlags = videoBox.querySelector('.skill-flags');
                       if (oldFlags) oldFlags.remove();
                       if (flagsHTML) {
                           videoBox.insertAdjacentHTML('beforeend', flagsHTML);
                           applyFlagTooltips(videoBox);
                       }
                   }
                   const key = `sub:${parentIdx}:${(s.name || s.n || ).trim()}`;
                   showSubVideo(key, videoBox);
                   Array.from(subBar.children).forEach(c => c.classList.remove('active'));
                   item.classList.add('active');
                   window.__lastActiveSkillIcon = item;
               });
               if (window.__globalSkillTooltip) {
                   const { show, hide, measureAndPos, lockUntil } = window.__globalSkillTooltip;
                   const label = item.title || ;
                   item.setAttribute('aria-label', label);
                   if (item.hasAttribute('title')) item.removeAttribute('title');
                   item.addEventListener('mouseenter', () => show(item, label));
                   item.addEventListener('mousemove', () => { if (performance.now() >= lockUntil.value) measureAndPos(item); });
                   item.addEventListener('click', () => { lockUntil.value = performance.now() + 240; measureAndPos(item); });
                   item.addEventListener('mouseleave', hide);
               }
               subBar.appendChild(item);
           });
           requestAnimationFrame(() => {
               subRail.classList.remove('collapsed');
               subRail.classList.remove('hidden');
               const h = subRail.offsetHeight || 48;
               if (spacer) spacer.style.height = h + 'px';
           });
       };
       api.hideAll = function (videoBox) {
           videoBox?.querySelectorAll('.skill-video[data-sub="1"]').forEach(v => {
               try { v.pause(); } catch { }
               v.style.display = 'none';
           });
       };
       window.renderSubskillsBarFrom = function (el, ctx) { api.renderBarFrom(el, ctx); };
       api.preloadAllSubskillImages = function () {
           const allSkillIcons = document.querySelectorAll('.icon-bar .skill-icon[data-subs]');
           const preloadPromises = [];
           let totalImages = 0;
           allSkillIcons.forEach(icon => {
               try {
                   const subsRaw = icon.getAttribute('data-subs');
                   if (!subsRaw) return;
                   const subs = JSON.parse(subsRaw);
                   if (!Array.isArray(subs)) return;
                   subs.forEach(s => {
                       if (s && s.icon) {
                           preloadPromises.push(preloadImage(s.icon));
                           totalImages++;
                       }
                       if (s && Array.isArray(s.subs)) {
                           s.subs.forEach(nested => {
                               if (nested && nested.icon) {
                                   preloadPromises.push(preloadImage(nested.icon));
                                   totalImages++;
                               }
                           });
                       }
                   });
               } catch (e) {
                   console.warn('Erro ao pré-carregar imagens de subskill:', e);
               }
           });
           if (totalImages > 0) {
               console.log(`🖼️ Pré-carregando ${totalImages} imagens de subskills...`);
               return Promise.all(preloadPromises).then(() => {
                   console.log(`✅ ${totalImages} imagens de subskills pré-carregadas com sucesso!`);
               });
           }
           return Promise.resolve();
       };
       if (document.readyState === 'loading') {
           document.addEventListener('DOMContentLoaded', () => {
               setTimeout(() => api.preloadAllSubskillImages(), 100);
           });
       } else {
           setTimeout(() => api.preloadAllSubskillImages(), 100);
       }
   })();

</script> <style>

   .subicon-bar {
       display: flex;
       gap: 10px;
       padding: 6px 6px;
       overflow-x: auto;
       /* Firefox */
       scrollbar-width: thin;
       scrollbar-color: #ababab transparent;
   }
   .subicon-bar::-webkit-scrollbar {
       height: 6px;
   }
   .subicon-bar::-webkit-scrollbar-thumb {
       background: #151515;
       border-radius: 3px;
   }
   .subicon {
       width: var(--icon-size, 42px);
       height: var(--icon-size, 42px);
       border-radius: var(--icon-radius, 10px);
       overflow: hidden;
       position: relative;
       flex: 0 0 auto;
       cursor: pointer;
       isolation: isolate;
   }
   .subicon img {
       width: 100%;
       height: 100%;
       object-fit: cover;
       display: block;
       border-radius: inherit;
   }
   .subicon::after {
       content: "";
       position: absolute;
       inset: 0;
       border-radius: inherit;
       box-shadow: inset 0 0 0 var(--icon-ring-w, 2px) var(--icon-idle, #cfcfcf);
       pointer-events: none;
       z-index: 2;
       transition: box-shadow .12s ease;
   }
   .subicon:hover::after {
       box-shadow: inset 0 0 0 var(--icon-ring-w, 2px) #e6e6e6;
   }
   .subicon.active::after {
       box-shadow: inset 0 0 0 var(--icon-ring-w, 2px) var(--icon-active, #FFD95A);
   }
   .subicon.active::before {
       content: "";
       position: absolute;
       inset: -4px;
       border-radius: calc(var(--icon-radius, 10px) + 4px);
       pointer-events: none;
       z-index: 1;
       opacity: 1;
       box-shadow: 0 0 12px 3px var(--icon-active-glow, rgba(255, 217, 90, .30)),
           0 0 0 calc(var(--icon-ring-w, 2px) * 2) var(--icon-active-ring, rgba(255, 217, 90, .50));
   }
   .top-rail.skills {
       position: relative;
       display: flex;
       flex-direction: column;
       align-items: center;
       overflow: visible;
   }
   .top-rail.skills .icon-bar {
       margin-bottom: 0;
       position: relative;
       z-index: 2;
   }
   .subskills-rail {
       position: absolute;
       left: 50%;
       transform: translateX(-50%);
       top: calc(100% - 1px);
       z-index: 3;
       display: inline-flex;
       justify-content: center;
       align-items: center;
       width: auto;
       max-width: 100%;
       padding: 3px 5px;
       background: rgba(0, 0, 0, .38);
       border: 1px solid rgba(255, 255, 255, .10);
       border-top: 1px solid rgba(255, 255, 255, .08);
       border-radius: 0 0 10px 10px;
       box-shadow: 0 3px 9px rgba(0, 0, 0, .22);
       -webkit-backdrop-filter: blur(2px);
       backdrop-filter: blur(2px);
       overflow: hidden;
       transition: opacity .14s ease, transform .14s ease;
       opacity: 1;
   }
   .subskills-rail::before {
       content: "";
       position: absolute;
       top: -6px;
       left: 0;
       right: 0;
       height: 6px;
       background: linear-gradient(to bottom, rgba(0, 0, 0, .20), rgba(0, 0, 0, 0));
       pointer-events: none;
   }
   .subskills-rail.collapsed {
       opacity: 0;
       pointer-events: none;
       transform: translate(-50%, -6px);
   }
   .subskills-rail.hidden {
       visibility: hidden;
   }
   .subskills-spacer {
       height: 0;
       transition: height .2s ease;
   }
   .subskills-rail .subicon-bar {
       display: inline-flex;
       align-items: center;
       gap: 0;
       overflow-x: auto;
       /* Firefox */
       scrollbar-width: thin;
       scrollbar-color: #ababab transparent;
   }
   .subskills-rail .subicon-bar::-webkit-scrollbar {
       height: 6px;
   }
   .subskills-rail .subicon-bar::-webkit-scrollbar-thumb {
       background: #151515;
       border-radius: 3px;
   }
   .subskills-rail .subicon {
       width: 42px;
       height: 42px;
       border-radius: 6px;
       position: relative;
       overflow: hidden;
       flex: 0 0 auto;
       cursor: pointer;
       isolation: isolate;
       -webkit-backface-visibility: hidden;
       backface-visibility: hidden;
       transform: translateZ(0);
   }
   .subskills-rail .subicon+.subicon {
       margin-left: 4px;
   }
   .subskills-rail .subicon img {
       width: 100%;
       height: 100%;
       object-fit: cover;
       display: block;
       border-radius: inherit;
   }
   .subskills-rail .subicon::after {
       content: "";
       position: absolute;
       inset: 0;
       border-radius: inherit;
       box-shadow: inset 0 0 0 2px var(--icon-idle, #cfcfcf);
       pointer-events: none;
       z-index: 2;
       transition: box-shadow .12s ease;
   }
   .subskills-rail .subicon:hover::after {
       box-shadow: inset 0 0 0 2px #e6e6e6;
   }
   .subskills-rail .subicon.active::after {
       box-shadow: inset 0 0 0 2px var(--icon-active, #FFD95A);
   }
   .video-container .skill-video {
       width: 100%;
       height: auto;
       aspect-ratio: 16 / 9;
       object-fit: cover;
       background: #000;
       border-radius: 10px;
   }
   @media (max-width: 900px) {
       .subskills-rail {
           position: static;
           transform: none;
           margin-top: -2px;
           border-top: 0;
           border-radius: 0 0 10px 10px;
       }
       .subskills-spacer {
           height: 0 !important;
       }
   }
   .skills-rail-wrap {
       position: relative;
       display: block;
       width: max-content;
       margin: 0 auto;
   }

</style>