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

De Wiki Gla
Ir para navegação Ir para pesquisar
m
m
 
(40 revisões intermediárias pelo mesmo usuário não estão sendo mostradas)
Linha 2: Linha 2:
<script>
<script>
     (function () {
     (function () {
        const SUBVIDEO_DEBUG = true;
        function logSubVideo(...args) {
            if (!SUBVIDEO_DEBUG) return;
            console.log('[SubVideo]', ...args);
        }
         const api = (window.__subskills ||= {});
         const api = (window.__subskills ||= {});
         const subCache = new Map();
         const subCache = new Map();
Linha 12: Linha 18:
         // ===== HERANÇA DE ATRIBUTOS: busca dados das skills principais =====
         // ===== HERANÇA DE ATRIBUTOS: busca dados das skills principais =====
         function getMainSkillsMap() {
         function getMainSkillsMap() {
             // Retorna cache se já foi construído
             // Retorna cache se já foi construído E tem dados
             if (cachedMainSkills) return cachedMainSkills;
             if (cachedMainSkills && cachedMainSkills.byIndex.size > 0) {
                return cachedMainSkills;
            }
 
             const maps = {
             const maps = {
                 byName: new Map(),
                 byName: new Map(),
                 byIndex: new Map()
                 byIndex: new Map()
             };
             };
             const icons = document.querySelectorAll('.icon-bar .skill-icon[data-nome]');
 
            // Busca skills com data-index (skills principais, não subskills)
             const icons = document.querySelectorAll('.icon-bar .skill-icon[data-index][data-nome]');
 
             icons.forEach(icon => {
             icons.forEach(icon => {
                 const name = (icon.dataset.nome || '').trim();
                 const name = (icon.dataset.nome || '').trim();
Linha 68: Linha 80:
                 maps.byName.set(name, data);
                 maps.byName.set(name, data);
                 if (index) {
                 if (index) {
                    // Guarda tanto como string quanto como número para compatibilidade
                     maps.byIndex.set(index, data);
                     maps.byIndex.set(index, data);
                    maps.byIndex.set(parseInt(index, 10), data);
                 }
                 }
             });
             });


Linha 83: Linha 98:


             let main = null;
             let main = null;
            // Tenta como string
             if (refIndex && mainSkills.byIndex.has(refIndex)) {
             if (refIndex && mainSkills.byIndex.has(refIndex)) {
                 main = mainSkills.byIndex.get(refIndex);
                 main = mainSkills.byIndex.get(refIndex);
            }
            // Tenta como número
            if (!main && refIndex) {
                const numIndex = parseInt(refIndex, 10);
                if (!isNaN(numIndex) && mainSkills.byIndex.has(numIndex)) {
                    main = mainSkills.byIndex.get(numIndex);
                }
             }
             }
             if (!main && name && mainSkills.byName.has(name)) {
             if (!main && name && mainSkills.byName.has(name)) {
Linha 91: Linha 114:


             // Se não tem main skill para herdar, retorna como está
             // Se não tem main skill para herdar, retorna como está
             if (!main) return sub;
             if (!main) {
                return sub;
            }


             // Se não tem nome mas tem refM, herda o nome da skill principal
             // Se não tem nome mas tem refM, herda o nome da skill principal
Linha 121: Linha 146:
                 : (window.mw && mw.config ? (mw.config.get('wgScript') || '/index.php') : '/index.php');
                 : (window.mw && mw.config ? (mw.config.get('wgScript') || '/index.php') : '/index.php');
             return `${base}?title=Especial:FilePath/${f}`;
             return `${base}?title=Especial:FilePath/${f}`;
        }
        function normalizeFileURL(raw, fallback = '') {
            if (!raw) return fallback;
            const val = String(raw).trim();
            if (!val) return fallback;
            if (/^(https?:)?\/\//i.test(val) || val.startsWith('data:') || val.includes('Especial:FilePath/')) {
                return val;
            }
            return filePathURL(val);
         }
         }


Linha 151: Linha 186:
                 level: 'Nível'
                 level: 'Nível'
             };
             };
        }
        // Verifica se o modo weapon está ativo
        function isWeaponModeOn() {
            try {
                return localStorage.getItem('glaWeaponEnabled') === '1';
            } catch (e) {
                return false;
            }
        }
        // Retorna os atributos corretos (weapon ou normal)
        function getEffectiveAttrs(s) {
            const weaponOn = isWeaponModeOn();
            if (weaponOn && s.weapon) {
                return {
                    powerpve: s.weapon.powerpve || s.powerpve,
                    powerpvp: s.weapon.powerpvp || s.powerpvp,
                    energy: s.weapon.energy || s.energy,
                    cooldown: s.weapon.cooldown || s.cooldown
                };
            }
            return {
                powerpve: s.powerpve,
                powerpvp: s.powerpvp,
                energy: s.energy,
                cooldown: s.cooldown
            };
        }
        // Retorna a descrição correta (weapon ou normal)
        // Aceita tanto desc_i18n quanto desc para compatibilidade
        function getEffectiveDesc(s) {
            const weaponOn = isWeaponModeOn();
            const raw = (document.documentElement.lang || 'pt').toLowerCase();
            const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
            // Para weapon: aceita tanto desc_i18n quanto desc
            if (weaponOn && s.weapon) {
                const wDesc = s.weapon.desc_i18n || s.weapon.desc;
                if (wDesc) {
                    return wDesc[lang] || wDesc.pt || wDesc.en || '';
                }
            }
            // Para descrição base: aceita tanto desc_i18n quanto desc
            const base = s.desc_i18n || s.desc;
            if (base) {
                return base[lang] || base.pt || base.en || '';
            }
            // Fallback para campos individuais (compatibilidade)
            const descI18n = {
                pt: s.descPt || '',
                en: s.descEn || '',
                es: s.descEs || '',
                pl: s.descPl || ''
            };
            return descI18n[lang] || descI18n.pt || '';
        }
        // Retorna o vídeo correto (weapon ou normal)
        function getEffectiveVideo(s) {
            const weaponOn = isWeaponModeOn();
            if (weaponOn && s.weapon && s.weapon.video && s.weapon.video.trim() !== '') {
                return s.weapon.video;
            }
            return s.video || '';
         }
         }


Linha 226: Linha 329:
         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 232: Linha 338:
                 rail.appendChild(subRail);
                 rail.appendChild(subRail);
             }
             }
             if (!subBar) {
             if (!subBar) {
                 subBar = document.createElement('div');
                 subBar = document.createElement('div');
Linha 237: Linha 344:
                 subRail.appendChild(subBar);
                 subRail.appendChild(subBar);
             }
             }
             if (!spacer) {
             if (!spacer) {
                 spacer = document.createElement('div');
                 spacer = document.createElement('div');
Linha 242: Linha 350:
                 rail.parentNode.insertBefore(spacer, rail.nextSibling);
                 rail.parentNode.insertBefore(spacer, rail.nextSibling);
             }
             }
             return rail;
             return rail;
         }
         }


        // CORREÇÃO 1: Função melhorada para mostrar vídeo sem recarregar
        function showSubVideo(key, videoBox) {
            logSubVideo('showSubVideo called', { key, videoBoxExists: !!videoBox });
            if (!videoBox) return;
            // Esconde todos os vídeos, sem pausar e sem resetar seu estado
            const allVideos = videoBox.querySelectorAll('video.skill-video');
            logSubVideo('hiding skill-video count', allVideos.length);
            allVideos.forEach(v => {
                v.style.display = 'none';
            });
            let v = null;
            // Primeiro tenta cache global (sincronizado com Character.Skills.html)
            if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) {
                v = window.__subskillVideosCache.get(key);
            }
            // Depois tenta cache local
            if (!v && subCache.has(key)) {
                v = subCache.get(key);
            }
            logSubVideo('showSubVideo resolved video element', {
                key,
                found: !!v,
                isConnected: v ? v.isConnected : false,
                parentNode: v ? (v.parentNode === videoBox) : false,
                fromGlobalCache: !!(window.__subskillVideosCache && window.__subskillVideosCache.has(key)),
                fromLocalCache: subCache.has(key)
            });
            if (!v) {
                logSubVideo('no video found for key, hiding box', key);
                videoBox.style.display = 'none';
                return;
            }
            // Verifica se o vídeo está no DOM, se não estiver, adiciona
            if (!v.isConnected || v.parentNode !== videoBox) {
                logSubVideo('video element not in DOM, re-adding', { key, isConnected: v.isConnected, parentNode: v.parentNode });
                if (v.parentNode) {
                    v.parentNode.removeChild(v);
                }
                videoBox.appendChild(v);
            }
            logSubVideo('about to show video', {
                key,
                readyState: v.readyState,
                paused: v.paused,
                currentTime: v.currentTime
            });
            // Mostra sem reiniciar vídeo
            videoBox.style.display = 'block';
            v.style.display = 'block';
            // Só roda play se o vídeo estiver parado e já tiver carregado
            if (v.paused && v.readyState >= 2) {
                v.play().then(() => {
                    logSubVideo('play() resolved', { key, currentTime: v.currentTime, readyState: v.readyState });
                }).catch(err => {
                    logSubVideo('play() rejected', { key, error: err });
                });
            }
        }
        // CORREÇÃO 2: Função melhorada para garantir cache de vídeos
         function ensureSubVideoCached(s, parentIdx, videoBox) {
         function ensureSubVideoCached(s, parentIdx, videoBox) {
            logSubVideo('ensureSubVideoCached called', {
                name: (s.name || s.n || '').trim(),
                parentIdx,
                hasVideo: !!(s.video && s.video.trim()),
                rawVideo: s.video
            });
             const key = `sub:${parentIdx}:${(s.name || s.n || '').trim()}`;
             const key = `sub:${parentIdx}:${(s.name || s.n || '').trim()}`;
            // Se já existe no cache global, usa ele
             if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) {
             if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) {
                logSubVideo('global cache hit in ensureSubVideoCached', key);
                 const precreated = window.__subskillVideosCache.get(key);
                 const precreated = window.__subskillVideosCache.get(key);
                // Verifica se o vídeo está no DOM, se não estiver, adiciona
                if (precreated && (!precreated.isConnected || precreated.parentNode !== videoBox)) {
                    logSubVideo('cached video not in DOM, re-adding', { key, isConnected: precreated.isConnected });
                    if (precreated.parentNode) {
                        precreated.parentNode.removeChild(precreated);
                    }
                    videoBox.appendChild(precreated);
                }
                 if (!subCache.has(key)) {
                 if (!subCache.has(key)) {
                     subCache.set(key, precreated);
                     subCache.set(key, precreated);
Linha 254: Linha 451:
                 return key;
                 return key;
             }
             }
             if (subCache.has(key)) return key;
 
             if (!s.video) return key;
             if (subCache.has(key)) {
                logSubVideo('local cache hit in ensureSubVideoCached', key);
                const cached = subCache.get(key);
                // Verifica se o vídeo está no DOM, se não estiver, adiciona
                if (cached && (!cached.isConnected || cached.parentNode !== videoBox)) {
                    logSubVideo('cached video not in DOM, re-adding', { key, isConnected: cached.isConnected });
                    if (cached.parentNode) {
                        cached.parentNode.removeChild(cached);
                    }
                    videoBox.appendChild(cached);
                }
                return key;
            }
             if (!s.video || s.video.trim() === '') return key;
            const videoFileName = s.video.trim();
            if (videoFileName === 'Nada.png' || videoFileName.toLowerCase().includes('nada.png')) {
                return key;
            }
 
            const videoURL = normalizeFileURL(videoFileName);
            if (!videoURL || videoURL.trim() === '') return key;
 
            logSubVideo('creating new base subskill video', { key, videoFileName, videoURL });


             const v = document.createElement('video');
             const v = document.createElement('video');
             v.className = 'skill-video';
             v.className = 'skill-video';
             v.dataset.sub = '1';
             v.dataset.sub = '1';
            v.dataset.cached = 'true';
             v.setAttribute('controls', '');
             v.setAttribute('controls', '');
             v.setAttribute('preload', 'auto');
             v.setAttribute('preload', 'auto');
             v.setAttribute('playsinline', '');
             v.setAttribute('playsinline', '');
             Object.assign(v.style, { display: 'none', width: '100%', height: 'auto', aspectRatio: '16/9', objectFit: 'cover' });
             Object.assign(v.style, { display: 'none', width: '100%', height: 'auto', aspectRatio: '16/9', objectFit: 'cover' });
            // Detectar formato do vídeo pela extensão
            const ext = (videoURL.split('.').pop() || '').toLowerCase().split('?')[0];
            const mimeTypes = {
                'mp4': 'video/mp4',
                'm4v': 'video/mp4',
                'webm': 'video/webm',
                'ogv': 'video/ogg',
                'ogg': 'video/ogg',
                'mov': 'video/quicktime'
            };
            const mimeType = mimeTypes[ext] || 'video/mp4';


             const src = document.createElement('source');
             const src = document.createElement('source');
             src.src = filePathURL(s.video);
             src.src = videoURL;
             src.type = 'video/webm';
             src.type = mimeType;
             v.appendChild(src);
             v.appendChild(src);
            // Fallback para Safari/iOS mais antigos
            v.setAttribute('webkit-playsinline', '');
            v.setAttribute('x-webkit-airplay', 'allow');
             videoBox.appendChild(v);
             videoBox.appendChild(v);
            // Adiciona ao cache local e global
             subCache.set(key, v);
             subCache.set(key, v);
             v.load();
             if (window.__subskillVideosCache) {
                window.__subskillVideosCache.set(key, v);
            }
 
            // Carrega o vídeo apenas uma vez - GARANTE: só chame load() se readyState === 0
            if (v.readyState === 0) {
                logSubVideo('calling load() for base subskill video', { key, readyState: v.readyState });
                v.load();
            }
 
             return key;
             return key;
         }
         }


         function showSubVideo(key, videoBox) {
        // Função para criar vídeo de weapon em cache
             videoBox.querySelectorAll('.skill-video').forEach(v => {
         function ensureSubVideoInCache(videoFileName, key, videoBox) {
                try { v.pause(); } catch { }
             logSubVideo('ensureSubVideoInCache called', { key, videoFileName });
                v.style.display = 'none';
 
            });
             // Verifica ambos os caches ANTES de criar
             let v = null;
             if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) {
             if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) {
                 v = window.__subskillVideosCache.get(key);
                 logSubVideo('global cache hit in ensureSubVideoInCache', key);
                const precreated = window.__subskillVideosCache.get(key);
                // Verifica se o vídeo está no DOM, se não estiver, adiciona
                if (precreated && (!precreated.isConnected || precreated.parentNode !== videoBox)) {
                    logSubVideo('cached weapon video not in DOM, re-adding', { key, isConnected: precreated.isConnected });
                    if (precreated.parentNode) {
                        precreated.parentNode.removeChild(precreated);
                    }
                    videoBox.appendChild(precreated);
                }
                if (!subCache.has(key)) {
                    subCache.set(key, precreated);
                }
                return key;
            }
 
            if (subCache.has(key)) {
                logSubVideo('local cache hit in ensureSubVideoInCache', key);
                const cached = subCache.get(key);
                // Verifica se o vídeo está no DOM, se não estiver, adiciona
                if (cached && (!cached.isConnected || cached.parentNode !== videoBox)) {
                    logSubVideo('cached weapon video not in DOM, re-adding', { key, isConnected: cached.isConnected });
                    if (cached.parentNode) {
                        cached.parentNode.removeChild(cached);
                    }
                    videoBox.appendChild(cached);
                }
                return key;
             }
             }
             if (!v) {
             if (!videoFileName || videoFileName.trim() === '') return key;
                v = subCache.get(key);
            const videoFile = videoFileName.trim();
            if (videoFile === 'Nada.png' || videoFile.toLowerCase().includes('nada.png')) {
                return key;
             }
             }
             if (!v) {
 
                 videoBox.style.display = 'none';
            const videoURL = normalizeFileURL(videoFile);
                 return;
             if (!videoURL || videoURL.trim() === '') return key;
 
            logSubVideo('creating new weapon subskill video', { key, videoFile, videoURL });
 
            const v = document.createElement('video');
            v.className = 'skill-video';
            v.dataset.sub = '1';
            v.dataset.weapon = '1';
            v.dataset.cached = 'true';
            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 ext = (videoURL.split('.').pop() || '').toLowerCase().split('?')[0];
            const mimeTypes = {
                 'mp4': 'video/mp4',
                'm4v': 'video/mp4',
                'webm': 'video/webm',
                'ogv': 'video/ogg',
                'ogg': 'video/ogg',
                'mov': 'video/quicktime'
            };
            const mimeType = mimeTypes[ext] || 'video/mp4';
 
            const src = document.createElement('source');
            src.src = videoURL;
            src.type = mimeType;
            v.appendChild(src);
 
            v.setAttribute('webkit-playsinline', '');
            v.setAttribute('x-webkit-airplay', 'allow');
            videoBox.appendChild(v);
 
            // Adiciona ao cache local e global
            subCache.set(key, v);
            if (window.__subskillVideosCache) {
                 window.__subskillVideosCache.set(key, v);
             }
             }
             videoBox.style.display = 'block';
 
             v.style.display = 'block';
             // Carrega apenas uma vez - GARANTE: só chame load() se readyState === 0
            try { v.currentTime = 0; } catch { }
             if (v.readyState === 0) {
            const suppress = document.body.dataset.suppressSkillPlay === '1';
                logSubVideo('calling load() for weapon subskill video', { key, readyState: v.readyState });
            if (!suppress) {
                 v.load();
                 v.play?.().catch(() => { });
             }
             }
            return key;
         }
         }


Linha 312: Linha 625:
             }
             }
             return true;
             return true;
        };
        // Função auxiliar para aplicar classes de weapon nas subskills renderizadas
        const applyWeaponClassesToSubskills = () => {
            const weaponOn = isWeaponModeOn();
            // Busca TODAS as subskills com weapon
            let weaponSubs = document.querySelectorAll('.subicon[data-weapon]');
            weaponSubs.forEach(el => {
                if (weaponOn) {
                    el.classList.add('has-weapon-available');
                    if (el.classList.contains('active')) {
                        el.classList.add('weapon-equipped');
                    }
                } else {
                    el.classList.remove('has-weapon-available');
                    el.classList.remove('weapon-equipped');
                }
            });
         };
         };


         api.renderBarFrom = function (el, { iconsBar, descBox, videoBox }) {
         api.renderBarFrom = function (el, { iconsBar, descBox, videoBox }) {
            logSubVideo('api.renderBarFrom called', {
                hasEl: !!el,
                hasIconsBar: !!iconsBar,
                hasDescBox: !!descBox,
                hasVideoBox: !!videoBox,
                parentIndex: el?.dataset?.index || 'unknown'
            });
             const rail = ensureRail(iconsBar);
             const rail = ensureRail(iconsBar);
             if (!rail) return;
             if (!rail) {
                logSubVideo('api.renderBarFrom: no rail found, returning');
                return;
            }


             const rawSubs = el.getAttribute('data-subs') || '';
             const rawSubs = el.getAttribute('data-subs') || '';
             const rawOrder = el.getAttribute('data-suborder') || '';
             const rawOrder = el.getAttribute('data-suborder') || '';
             const parentIdx = el.dataset.index || '';
             const parentIdx = el.dataset.index || '';
            logSubVideo('api.renderBarFrom: parsing subs', {
                parentIdx,
                hasRawSubs: !!rawSubs.trim(),
                rawSubsLength: rawSubs.length
            });


             if (!rawSubs.trim()) {
             if (!rawSubs.trim()) {
                 subRail.classList.add('collapsed');
                 if (subRail) subRail.classList.add('collapsed');
                 subRail.classList.add('hidden');
                 if (subRail) subRail.classList.add('hidden');
                 subBar.innerHTML = '';
                 if (subBar) subBar.innerHTML = '';
                 if (spacer) spacer.style.height = '0px';
                 if (spacer) spacer.style.height = '0px';
                 return;
                 return;
Linha 332: Linha 681:
             let subs;
             let subs;
             try { subs = JSON.parse(rawSubs); } catch { subs = []; }
             try { subs = JSON.parse(rawSubs); } catch { subs = []; }
            // NORMALIZADOR: Converte weaponPacked para weapon se necessário
            subs = subs.map(sub => {
                // Se tem weaponPacked mas não tem weapon, tenta parsear
                if (sub.weaponPacked && !sub.weapon && typeof sub.weaponPacked === 'string' && sub.weaponPacked.trim() !== '') {
                    const parts = sub.weaponPacked.split('~');
                    if (parts.length >= 2) {
                        sub.weapon = {
                            icon: parts[0] || 'Nada.png',
                            powerpve: parts[1] || null,
                            powerpvp: parts[2] || null,
                            cooldown: parts[3] || null,
                            video: parts[4] || '',
                            energy: parts[5] || null
                        };
                        // Remove valores vazios
                        Object.keys(sub.weapon).forEach(k => {
                            if (sub.weapon[k] === '' || sub.weapon[k] === null) {
                                delete sub.weapon[k];
                            }
                        });
                    }
                }
                // Garante que weapon seja objeto válido
                if (sub.weapon && typeof sub.weapon === 'string') {
                    // Tenta parsear como JSON primeiro
                    try {
                        sub.weapon = JSON.parse(sub.weapon);
                    } catch {
                        // Se falhar, tenta formato ~
                        const parts = sub.weapon.split('~');
                        if (parts.length >= 2) {
                            sub.weapon = {
                                icon: parts[0] || 'Nada.png',
                                powerpve: parts[1] || null,
                                powerpvp: parts[2] || null,
                                cooldown: parts[3] || null,
                                video: parts[4] || '',
                                energy: parts[5] || null
                            };
                            Object.keys(sub.weapon).forEach(k => {
                                if (sub.weapon[k] === '' || sub.weapon[k] === null) {
                                    delete sub.weapon[k];
                                }
                            });
                        } else {
                            sub.weapon = null;
                        }
                    }
                }
                return sub;
            });
             if (!Array.isArray(subs) || subs.length === 0) {
             if (!Array.isArray(subs) || subs.length === 0) {
                 subRail.classList.add('collapsed');
                 subRail.classList.add('collapsed');
Linha 343: Linha 745:
             const mainSkills = getMainSkillsMap();
             const mainSkills = getMainSkillsMap();


             // Aplica herança ANTES de processar - isso resolve nome, icon, etc.
             // Aplica herança ANTES de processar
             subs = subs.map(sub => applyInheritance(sub, mainSkills));
             subs = subs.map(sub => applyInheritance(sub, mainSkills));


             // Remove subskills que ficaram sem nome após herança (herança falhou)
             // Remove subskills que ficaram sem nome após herança
             subs = subs.filter(s => (s.name || s.n || '').trim() !== '');
             subs = subs.filter(s => (s.name || s.n || '').trim() !== '');


Linha 364: Linha 766:
             }
             }


            // CORREÇÃO 3: Pré-criar todos os vídeos ANTES de renderizar os ícones
            logSubVideo('pre-creating all subskill videos before render', { parentIdx, orderCount: order.length });
             order.forEach(nm => {
             order.forEach(nm => {
                 const s = subs.find(x => (x.name || x.n || '') === nm);
                 const s = subs.find(x => (x.name || x.n || '') === nm);
                 if (s) {
                 if (s) {
                     if (s.video) ensureSubVideoCached(s, parentIdx, videoBox);
                    // Pré-cria vídeo normal
                     if (s.video) {
                        logSubVideo('pre-creating base video in renderBarFrom', { subName: (s.name || s.n || '').trim(), parentIdx });
                        ensureSubVideoCached(s, parentIdx, videoBox);
                    }
                    // Pré-cria vídeo de weapon se existir
                    if (s.weapon && s.weapon.video) {
                        const weaponKey = `sub:${parentIdx}:${(s.name || s.n || '').trim()}:weapon`;
                        logSubVideo('pre-creating weapon video in renderBarFrom', { subName: (s.name || s.n || '').trim(), parentIdx, weaponKey });
                        ensureSubVideoInCache(s.weapon.video, weaponKey, videoBox);
                    }
                     if (s.icon) preloadImage(s.icon);
                     if (s.icon) preloadImage(s.icon);
                 }
                 }
Linha 374: Linha 788:
             order.forEach(nm => {
             order.forEach(nm => {
                 const s = subs.find(x => (x.name || x.n || '') === nm);
                 const s = subs.find(x => (x.name || x.n || '') === nm);
                 if (!s) return;
                 if (!s) {
                    return;
                }


                 const item = document.createElement('div');
                 const item = document.createElement('div');
Linha 382: Linha 798:
                 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);
                logSubVideo('creating subicon element', {
                    subName: (s.name || s.n || '').trim(),
                    parentIdx,
                    className: item.className
                });


                 const img = document.createElement('img');
                 const img = document.createElement('img');
Linha 388: Linha 810:
                 item.appendChild(img);
                 item.appendChild(img);


                // Verifica weapon de forma mais robusta
                const hasWeapon = s.weapon && (
                    (typeof s.weapon === 'object' && Object.keys(s.weapon).length > 0) ||
                    (typeof s.weapon === 'string' && s.weapon.trim() !== '')
                );
                const subName = (s.name || s.n || '').trim();
                if (hasWeapon) {
                    // Normaliza weapon se for string
                    let weaponObj = s.weapon;
                    if (typeof weaponObj === 'string') {
                        try {
                            weaponObj = JSON.parse(weaponObj);
                        } catch {
                            // Se falhar, tenta formato ~
                            const parts = weaponObj.split('~');
                            if (parts.length >= 2) {
                                weaponObj = {
                                    icon: parts[0] || 'Nada.png',
                                    powerpve: parts[1] || null,
                                    powerpvp: parts[2] || null,
                                    cooldown: parts[3] || null,
                                    video: parts[4] || '',
                                    energy: parts[5] || null
                                };
                                Object.keys(weaponObj).forEach(k => {
                                    if (weaponObj[k] === '' || weaponObj[k] === null) {
                                        delete weaponObj[k];
                                    }
                                });
                            } else {
                                weaponObj = null;
                            }
                        }
                    }
                    if (weaponObj && typeof weaponObj === 'object' && Object.keys(weaponObj).length > 0) {
                        try {
                            item.dataset.weapon = JSON.stringify(weaponObj);
                        } catch (e) {
                            console.error('[Subskills] Erro ao serializar weapon de subskill', subName, e);
                        }
                        // Aplica classe inicial se o toggle já está ativo
                        if (isWeaponModeOn()) {
                            item.classList.add('has-weapon-available');
                        }
                    }
                }
                // CORREÇÃO 4: Evento de clique otimizado para não recarregar vídeos
                logSubVideo('attaching click handler to subicon', {
                    subName: (s.name || s.n || '').trim(),
                    parentIdx,
                    itemClassName: item.className
                });
                 item.addEventListener('click', () => {
                 item.addEventListener('click', () => {
                     const L = getLabels();
                     const L = getLabels();
                     const descI18n = {
                     const subName = (s.name || s.n || '').trim();
                        pt: s.descPt || (s.desc_i18n && s.desc_i18n.pt) || '',
 
                         en: s.descEn || (s.desc_i18n && s.desc_i18n.en) || '',
                    logSubVideo('subicon click HANDLER EXECUTED', {
                         es: s.descEs || (s.desc_i18n && s.desc_i18n.es) || '',
                        subName,
                         pl: s.descPl || (s.desc_i18n && s.desc_i18n.pl) || ''
                        parentIdx,
                     };
                         weaponMode: isWeaponModeOn(),
                        hasWeaponDataAttr: !!item.dataset.weapon,
                         rawWeaponDataset: item.dataset.weapon || null
                    });
 
                    // Lê weapon diretamente do atributo data-weapon
                    let subWeaponData = null;
                    if (item.dataset.weapon) {
                         try {
                            subWeaponData = JSON.parse(item.dataset.weapon);
                        } catch (e) {
                            subWeaponData = null;
                        }
                    }
                    const hasSubWeapon = !!subWeaponData;
 
                    const weaponOn = isWeaponModeOn();
                    const weaponEquipped = weaponOn && hasSubWeapon && subWeaponData;
 
                    logSubVideo('weapon status for subclick', {
                        subName,
                        weaponEquipped,
                        hasSubWeaponData: !!subWeaponData
                     });
 
                    // Determina descrição (weapon ou normal)
                     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');
                     const chosen = descI18n[lang] || descI18n.pt || '';
 
                     let chosen = '';
                    if (weaponEquipped && subWeaponData) {
                        // Descrição do weapon
                        const weaponDescPack = subWeaponData.desc_i18n || subWeaponData.desc || {};
                        chosen = weaponDescPack[lang] || weaponDescPack.pt || weaponDescPack.en || '';
                    } else {
                        // Descrição normal
                        const base = s.desc_i18n || s.desc || {};
                        chosen = base[lang] || base.pt || base.en || (s.descPt || '');
                    }
 
                    // Determina atributos (weapon ou normal)
                    let attrsObj = {
                        powerpve: s.powerpve,
                        powerpvp: s.powerpvp,
                        energy: s.energy,
                        cooldown: s.cooldown
                    };
                    if (weaponEquipped && subWeaponData) {
                        attrsObj = {
                            powerpve: subWeaponData.powerpve || s.powerpve,
                            powerpvp: subWeaponData.powerpvp || s.powerpvp,
                            energy: subWeaponData.energy || s.energy,
                            cooldown: subWeaponData.cooldown || s.cooldown
                        };
                    }
 
                    // Level (weapon ou normal)
                    const level = (weaponEquipped && subWeaponData && subWeaponData.level)
                        ? subWeaponData.level.toString().trim()
                        : (s.level || '').toString().trim();
 
                    let flagsHTML = '';
                    if (Array.isArray(s.flags) && s.flags.length > 0) {
                        flagsHTML = renderFlagsRow(s.flags);
                    }


                     if (descBox) {
                     if (descBox) {
                        const level = (s.level || '').toString().trim();
                         descBox.innerHTML = `<div class="skill-title"><h3>${s.name || nm}</h3></div>${level ? `<div class="skill-level-line"><span class="attr-label">${L.level} ${level}</span></div>` : ''}${renderSubAttrs(attrsObj, L)}<div class="desc">${chosen.replace(/'''(.*?)'''/g, '<b>$1</b>')}</div>`;
                        let flagsHTML = '';
                        if (Array.isArray(s.flags) && s.flags.length > 0) {
                            flagsHTML = renderFlagsRow(s.flags);
                        }
                         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>`;
                     }
                     }


Linha 418: Linha 953:
                     }
                     }


                     const key = `sub:${parentIdx}:${(s.name || s.n || '').trim()}`;
                    // Vídeo (weapon ou normal) - usa cache já criado
                    showSubVideo(key, videoBox);
                     const effectiveVideo = getEffectiveVideo(s);
                     Array.from(subBar.children).forEach(c => c.classList.remove('active'));
 
                    logSubVideo('effectiveVideo for sub', {
                        subName,
                        parentIdx,
                        effectiveVideo,
                        weaponEquipped,
                        rawBaseVideo: s.video,
                        rawWeaponVideo: subWeaponData?.video
                    });
 
                    if (!effectiveVideo || effectiveVideo.trim() === '' || effectiveVideo === 'Nada.png' || effectiveVideo.toLowerCase().includes('nada.png')) {
                        if (videoBox) videoBox.style.display = 'none';
                    } else {
                        const videoKey = (weaponEquipped && subWeaponData && subWeaponData.video && subWeaponData.video.trim() !== '')
                            ? `sub:${parentIdx}:${subName}:weapon`
                            : `sub:${parentIdx}:${subName}`;
 
                        logSubVideo('computed videoKey for subclick', { videoKey });
                        logSubVideo('calling showSubVideo from click handler', { videoKey });
 
                        // NÃO cria novo vídeo - apenas mostra o que já está em cache
                        showSubVideo(videoKey, videoBox);
                    }
 
                     Array.from(subBar.children).forEach(c => {
                        c.classList.remove('active');
                        c.classList.remove('weapon-equipped');
                    });
                     item.classList.add('active');
                     item.classList.add('active');
                    // Aplica weapon-equipped se tem weapon e está ativo
                    if (weaponEquipped) {
                        item.classList.add('weapon-equipped');
                    }
                     window.__lastActiveSkillIcon = item;
                     window.__lastActiveSkillIcon = item;
                 });
                 });
Linha 437: Linha 1 004:


                 subBar.appendChild(item);
                 subBar.appendChild(item);
                logSubVideo('subicon appended to subBar', {
                    subName: (s.name || s.n || '').trim(),
                    parentIdx,
                    subBarChildrenCount: subBar.children.length
                });
             });
             });


             requestAnimationFrame(() => {
             // Aplica classes de weapon nas subskills recém-renderizadas
                subRail.classList.remove('collapsed');
            setTimeout(() => {
                subRail.classList.remove('hidden');
                applyWeaponClassesToSubskills();
                const h = subRail.offsetHeight || 48;
 
                if (spacer) spacer.style.height = h + 'px';
                // Dispara evento para notificar que subskills estão prontas
             });
                window.dispatchEvent(new CustomEvent('gla:subskills:ready', { detail: { count: order.length } }));
 
                // Remove listener anterior se existir (evita duplicação)
                if (subBar._weaponToggleListener) {
                    window.removeEventListener('gla:weaponToggled', subBar._weaponToggleListener);
                }
 
                // Cria novo listener que opera no subBar atual
                subBar._weaponToggleListener = (e) => {
                    const enabled = e.detail?.enabled ?? false;
 
                    // Aplica classes usando a função auxiliar
                    applyWeaponClassesToSubskills();
 
                    // Atualiza a subskill ativa se houver
                    setTimeout(() => {
                        const activeSub = subBar.querySelector('.subicon[data-weapon].active')
                            || subBar.querySelector('.subicon.active');
                        if (activeSub) {
                            activeSub.dispatchEvent(new Event('click', { bubbles: true }));
                        } else {
                            api.refreshCurrentSubSafe();
                        }
                    }, 50);
                };
 
                window.addEventListener('gla:weaponToggled', subBar._weaponToggleListener);
 
                requestAnimationFrame(() => {
                    subRail.classList.remove('collapsed');
                    subRail.classList.remove('hidden');
                    const h = subRail.offsetHeight || 48;
                    if (spacer) spacer.style.height = h + 'px';
                });
             }, 10);
         };
         };


         api.hideAll = function (videoBox) {
         api.hideAll = function (videoBox) {
             videoBox?.querySelectorAll('.skill-video[data-sub="1"]').forEach(v => {
             const videos = videoBox?.querySelectorAll('.skill-video[data-sub="1"]') || [];
            logSubVideo('api.hideAll called', { videoBoxExists: !!videoBox, videoCount: videos.length });
            videos.forEach(v => {
                 try { v.pause(); } catch { }
                 try { v.pause(); } catch { }
                 v.style.display = 'none';
                 v.style.display = 'none';
Linha 483: Linha 1 091:
                     });
                     });
                 } catch (e) {
                 } catch (e) {
                    console.warn('Erro ao pré-carregar imagens de subskill:', e);
                 }
                 }
             });
             });


             if (totalImages > 0) {
             if (totalImages > 0) {
                console.log(`🖼️ Pré-carregando ${totalImages} imagens de subskills...`);
                 return Promise.all(preloadPromises).then(() => {
                 return Promise.all(preloadPromises).then(() => {
                    console.log(`✅ ${totalImages} imagens de subskills pré-carregadas com sucesso!`);
                 });
                 });
             }
             }
Linha 496: Linha 1 101:
         };
         };


         // Inicialização: constrói cache das skills principais e pré-carrega imagens
         // CORREÇÃO 5: Nova função para pré-carregar vídeos de subskills
        api.preloadAllSubskillVideos = function () {
            const allSkillIcons = document.querySelectorAll('.icon-bar .skill-icon[data-subs]');
            allSkillIcons.forEach(icon => {
                try {
                    const subsRaw = icon.getAttribute('data-subs');
                    if (!subsRaw) return;
                    const subs = JSON.parse(subsRaw);
                    if (!Array.isArray(subs)) return;
                    const parentIdx = icon.dataset.index || '';
 
                    logSubVideo('preloadAllSubskillVideos entry', {
                        parentIdx,
                        hasSubs: Array.isArray(subs),
                        subsCount: Array.isArray(subs) ? subs.length : 0
                    });
 
                    subs.forEach(s => {
                        // Pré-carrega vídeo normal
                        if (s.video && s.video.trim() !== '' && s.video !== 'Nada.png' && !s.video.toLowerCase().includes('nada.png')) {
                            logSubVideo('preloading base subskill video (link preload)', {
                                parentIdx,
                                subName: (s.name || s.n || '').trim(),
                                video: s.video
                            });
                            const videoURL = normalizeFileURL(s.video);
                            if (videoURL) {
                                // Pré-carregar via link preload
                                const link = document.createElement('link');
                                link.rel = 'preload';
                                link.as = 'video';
                                link.href = videoURL;
                                link.crossOrigin = 'anonymous';
                                document.head.appendChild(link);
                            }
                        }
 
                        // Pré-carrega vídeo de weapon
                        if (s.weapon && typeof s.weapon === 'object' && s.weapon.video && s.weapon.video.trim() !== '' && s.weapon.video !== 'Nada.png' && !s.weapon.video.toLowerCase().includes('nada.png')) {
                            logSubVideo('preloading weapon subskill video (link preload)', {
                                parentIdx,
                                subName: (s.name || s.n || '').trim(),
                                weaponVideo: s.weapon.video
                            });
                            const weaponVideoURL = normalizeFileURL(s.weapon.video);
                            if (weaponVideoURL) {
                                const link = document.createElement('link');
                                link.rel = 'preload';
                                link.as = 'video';
                                link.href = weaponVideoURL;
                                link.crossOrigin = 'anonymous';
                                document.head.appendChild(link);
                            }
                        }
                    });
                } catch (e) {
                }
            });
        };
 
        // CORREÇÃO 6: Função para sincronizar cache de vídeos
        function syncVideoCaches() {
            if (!window.__subskillVideosCache) {
                window.__subskillVideosCache = new Map();
            }
 
            // Copiar vídeos do cache local para o global
            subCache.forEach((video, key) => {
                if (!window.__subskillVideosCache.has(key)) {
                    window.__subskillVideosCache.set(key, video);
                }
            });
 
            // Copiar vídeos do global para o local
            if (window.__subskillVideosCache) {
                window.__subskillVideosCache.forEach((video, key) => {
                    if (!subCache.has(key)) {
                        subCache.set(key, video);
                    }
                });
            }
 
            logSubVideo('syncVideoCaches', {
                localSize: subCache.size,
                globalSize: window.__subskillVideosCache ? window.__subskillVideosCache.size : 0
            });
        }
 
        // Inicialização
         function init() {
         function init() {
             // Constrói cache das skills principais ANTES de qualquer interação
             // Constrói cache das skills principais
             getMainSkillsMap();
             getMainSkillsMap();
             // Pré-carrega imagens das subskills
             // Pré-carrega imagens das subskills
             api.preloadAllSubskillImages();
             api.preloadAllSubskillImages();
            // CORREÇÃO 7: Pré-carrega vídeos das subskills
            api.preloadAllSubskillVideos();
            // Sincroniza caches periodicamente
            setInterval(syncVideoCaches, 2000);
            // Escuta mudanças no localStorage
            window.addEventListener('storage', (e) => {
                if (e.key === 'glaWeaponEnabled') {
                    setTimeout(() => api.refreshCurrentSubSafe(), 50);
                }
            });
            // LISTENER GLOBAL: Escuta evento de toggle
            window.addEventListener('gla:weaponToggled', (e) => {
                const enabled = e.detail?.enabled ?? false;
                // Tenta aplicar classes imediatamente
                applyWeaponClassesToSubskills();
                // Atualiza a subskill ativa se houver
                setTimeout(() => {
                    const activeSub = document.querySelector('.subicon[data-weapon].active')
                        || document.querySelector('.subicon.active');
                    if (activeSub) {
                        activeSub.dispatchEvent(new Event('click', { bubbles: true }));
                    } else {
                        api.refreshCurrentSubSafe();
                    }
                }, 50);
            });
            // LISTENER: Escuta quando subskills estão prontas
            window.addEventListener('gla:subskills:ready', (e) => {
                // Aplica classes de weapon se o toggle estiver ativo
                applyWeaponClassesToSubskills();
            });
         }
         }


Linha 739: Linha 1 471:
         width: max-content;
         width: max-content;
         margin: 0 auto;
         margin: 0 auto;
    }
    /* Subskills com arma disponível - borda vermelha quando inativa */
    .subicon.has-weapon-available:not(.active)::after {
        box-shadow: inset 0 0 0 2px rgba(220, 70, 70, 0.8) !important;
    }
    /* Subskill com arma ATIVA - laranja/coral vibrante */
    .subicon.has-weapon-available.active::after {
        box-shadow: inset 0 0 0 2px #FF7043 !important;
    }
    .subicon.has-weapon-available.active::before {
        box-shadow: 0 0 12px 3px rgba(255, 112, 67, 0.35), 0 0 0 4px rgba(255, 112, 67, 0.25) !important;
    }
    /* Modo arma ON - efeito animado nos subicons */
    .top-rail.skills.weapon-mode-on .subicon.has-weapon-available {
        position: relative;
    }
    .top-rail.skills.weapon-mode-on .subicon.has-weapon-available::after {
        box-shadow: none !important;
        background: linear-gradient(90deg,
                rgba(255, 80, 80, 0.9) 0%,
                rgba(255, 120, 60, 1) 25%,
                rgba(255, 80, 80, 0.9) 50%,
                rgba(255, 120, 60, 1) 75%,
                rgba(255, 80, 80, 0.9) 100%) !important;
        background-size: 400% 100% !important;
        animation: weapon-subicon-border-scan 4s linear infinite !important;
        -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0) !important;
        mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0) !important;
        -webkit-mask-composite: xor !important;
        mask-composite: exclude !important;
        padding: 2px !important;
    }
    .top-rail.skills.weapon-mode-on .subicon.has-weapon-available::before {
        content: "";
        position: absolute;
        inset: -4px;
        border-radius: calc(6px + 4px);
        pointer-events: none;
        z-index: 1;
        animation: weapon-subicon-pulse 3s ease-in-out infinite !important;
    }
    /* Subskill ativa com arma - mais intenso */
    .top-rail.skills.weapon-mode-on .subicon.has-weapon-available.active::after {
        background: linear-gradient(90deg,
                rgba(255, 87, 34, 1) 0%,
                rgba(255, 140, 60, 1) 25%,
                rgba(255, 87, 34, 1) 50%,
                rgba(255, 140, 60, 1) 75%,
                rgba(255, 87, 34, 1) 100%) !important;
    }
    .top-rail.skills.weapon-mode-on .subicon.has-weapon-available.active::before {
        box-shadow: 0 0 14px 4px rgba(255, 87, 34, 0.4), 0 0 0 4px rgba(255, 87, 34, 0.3) !important;
        animation: weapon-subicon-pulse 2.5s ease-in-out infinite !important;
    }
    @keyframes weapon-subicon-border-scan {
        0% {
            background-position: 0% 0%;
        }
        100% {
            background-position: 400% 0%;
        }
    }
    @keyframes weapon-subicon-pulse {
        0%,
        100% {
            opacity: 0.5;
            box-shadow: 0 0 8px rgba(255, 80, 80, 0.25);
        }
        50% {
            opacity: 1;
            box-shadow: 0 0 16px rgba(255, 80, 80, 0.45);
        }
     }
     }
</style>
</style>

Edição atual tal como às 17h18min de 4 de dezembro de 2025

<script>

   (function () {
       const SUBVIDEO_DEBUG = true;
       function logSubVideo(...args) {
           if (!SUBVIDEO_DEBUG) return;
           console.log('[SubVideo]', ...args);
       }
       const api = (window.__subskills ||= {});
       const subCache = new Map();
       const imagePreloadCache = new Map();
       let subRail, subBar, spacer;
       // Cache das skills principais (capturado na carga da página)
       let cachedMainSkills = null;
       // ===== HERANÇA DE ATRIBUTOS: busca dados das skills principais =====
       function getMainSkillsMap() {
           // Retorna cache se já foi construído E tem dados
           if (cachedMainSkills && cachedMainSkills.byIndex.size > 0) {
               return cachedMainSkills;
           }
           const maps = {
               byName: new Map(),
               byIndex: new Map()
           };
           // Busca skills com data-index (skills principais, não subskills)
           const icons = document.querySelectorAll('.icon-bar .skill-icon[data-index][data-nome]');
           icons.forEach(icon => {
               const name = (icon.dataset.nome || ).trim();
               if (!name) return;
               // Extrai atributos do data-atr (formato: "pve, pvp, energy, cd")
               const atrRaw = icon.dataset.atr || ;
               const parts = atrRaw.split(',').map(x => (x || ).trim());
               const powerpve = parts[0] && parts[0] !== '-' ? parts[0] : ;
               const powerpvp = parts[1] && parts[1] !== '-' ? parts[1] : ;
               const energy = parts[2] && parts[2] !== '-' ? parts[2] : ;
               const cooldown = parts[3] && parts[3] !== '-' ? parts[3] : ;
               // Nome original do arquivo de ícone (armazenado no dataset pelo widget de skills)
               let iconFile = (icon.dataset.iconFile || ).trim();
               if (!iconFile) {
                   const imgSrc = icon.querySelector('img')?.src || ;
                   const iconMatch = imgSrc.match(/(?:FilePath|images)\/([^\/?]+)$/);
                   iconFile = iconMatch ? decodeURIComponent(iconMatch[1]) : ;
               }
               // Nome original do arquivo de vídeo (caso exista)
               let videoFile = (icon.dataset.videoFile || ).trim();
               if (!videoFile) {
                   const videoUrl = icon.dataset.video || ;
                   const videoMatch = videoUrl.match(/FilePath\/([^&?]+)/);
                   videoFile = videoMatch ? decodeURIComponent(videoMatch[1]) : ;
               }
               const index = (icon.dataset.index || ).trim();
               const data = {
                   name: name,  // Inclui o nome para herança completa
                   icon: iconFile,
                   level: icon.dataset.level || ,
                   video: videoFile,
                   powerpve: powerpve,
                   powerpvp: powerpvp,
                   cooldown: cooldown,
                   energy: energy
               };
               // Mantém descrições caso precise como fallback extra
               if (icon.dataset.descPt) data.descPt = icon.dataset.descPt;
               if (icon.dataset.descEn) data.descEn = icon.dataset.descEn;
               if (icon.dataset.descEs) data.descEs = icon.dataset.descEs;
               if (icon.dataset.descPl) data.descPl = icon.dataset.descPl;
               maps.byName.set(name, data);
               if (index) {
                   // Guarda tanto como string quanto como número para compatibilidade
                   maps.byIndex.set(index, data);
                   maps.byIndex.set(parseInt(index, 10), data);
               }
           });
           // Cacheia para uso futuro (importante: as skills principais não mudam)
           cachedMainSkills = maps;
           return maps;
       }
       // Aplica herança COMPLETA: se subskill só tem refM, busca TUDO da skill principal
       function applyInheritance(sub, mainSkills) {
           let name = (sub.name || sub.n || ).trim();
           const refIndex = ((sub.refM || sub.m || sub.M || ) + ).trim();
           let main = null;
           // Tenta como string
           if (refIndex && mainSkills.byIndex.has(refIndex)) {
               main = mainSkills.byIndex.get(refIndex);
           }
           // Tenta como número
           if (!main && refIndex) {
               const numIndex = parseInt(refIndex, 10);
               if (!isNaN(numIndex) && mainSkills.byIndex.has(numIndex)) {
                   main = mainSkills.byIndex.get(numIndex);
               }
           }
           if (!main && name && mainSkills.byName.has(name)) {
               main = mainSkills.byName.get(name);
           }
           // Se não tem main skill para herdar, retorna como está
           if (!main) {
               return sub;
           }
           // Se não tem nome mas tem refM, herda o nome da skill principal
           if (!name && refIndex && main.name) {
               name = main.name;
           }
           return {
               ...sub,
               name: name || main.name || sub.name,
               icon: (sub.icon && sub.icon !== 'Nada.png' && sub.icon !== ) ? sub.icon : (main.icon || 'Nada.png'),
               level: sub.level || main.level,
               video: sub.video || main.video,
               powerpve: sub.powerpve || main.powerpve,
               powerpvp: sub.powerpvp || main.powerpvp,
               cooldown: sub.cooldown || main.cooldown,
               energy: sub.energy || main.energy,
               descPt: sub.descPt || (sub.desc_i18n?.pt) || main.descPt,
               descEn: sub.descEn || (sub.desc_i18n?.en) || main.descEn,
               descEs: sub.descEs || (sub.desc_i18n?.es) || main.descEs,
               descPl: sub.descPl || (sub.desc_i18n?.pl) || main.descPl
           };
       }
       function filePathURL(fileName) {
           const f = encodeURIComponent((fileName || 'Nada.png').replace(/^Arquivo:|^File:/, ));
           const base = (window.mw && mw.util && typeof mw.util.wikiScript === 'function')
               ? mw.util.wikiScript()
               : (window.mw && mw.config ? (mw.config.get('wgScript') || '/index.php') : '/index.php');
           return `${base}?title=Especial:FilePath/${f}`;
       }
       function normalizeFileURL(raw, fallback = ) {
           if (!raw) return fallback;
           const val = String(raw).trim();
           if (!val) return fallback;
           if (/^(https?:)?\/\//i.test(val) || val.startsWith('data:') || val.includes('Especial:FilePath/')) {
               return val;
           }
           return filePathURL(val);
       }
       function preloadImage(iconFile) {
           const url = filePathURL(iconFile || 'Nada.png');
           if (imagePreloadCache.has(url)) {
               return imagePreloadCache.get(url);
           }
           const promise = new Promise((resolve, reject) => {
               const img = new Image();
               img.onload = () => resolve(url);
               img.onerror = () => resolve(url);
               img.src = url;
           });
           imagePreloadCache.set(url, promise);
           return promise;
       }
       function getLabels() {
           const skillsRoot = document.getElementById('skills');
           const i18nMap = skillsRoot ? JSON.parse(skillsRoot.dataset.i18nAttrs || '{}') : {};
           const raw = (document.documentElement.lang || skillsRoot?.dataset.i18nDefault || 'pt').toLowerCase();
           const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
           return i18nMap[lang] || i18nMap.pt || {
               cooldown: 'Recarga',
               energy_gain: 'Ganho de energia',
               energy_cost: 'Custo de energia',
               power: 'Poder',
               power_pvp: 'Poder PvP',
               level: 'Nível'
           };
       }
       // Verifica se o modo weapon está ativo
       function isWeaponModeOn() {
           try {
               return localStorage.getItem('glaWeaponEnabled') === '1';
           } catch (e) {
               return false;
           }
       }
       // Retorna os atributos corretos (weapon ou normal)
       function getEffectiveAttrs(s) {
           const weaponOn = isWeaponModeOn();
           if (weaponOn && s.weapon) {
               return {
                   powerpve: s.weapon.powerpve || s.powerpve,
                   powerpvp: s.weapon.powerpvp || s.powerpvp,
                   energy: s.weapon.energy || s.energy,
                   cooldown: s.weapon.cooldown || s.cooldown
               };
           }
           return {
               powerpve: s.powerpve,
               powerpvp: s.powerpvp,
               energy: s.energy,
               cooldown: s.cooldown
           };
       }
       // Retorna a descrição correta (weapon ou normal)
       // Aceita tanto desc_i18n quanto desc para compatibilidade
       function getEffectiveDesc(s) {
           const weaponOn = isWeaponModeOn();
           const raw = (document.documentElement.lang || 'pt').toLowerCase();
           const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
           // Para weapon: aceita tanto desc_i18n quanto desc
           if (weaponOn && s.weapon) {
               const wDesc = s.weapon.desc_i18n || s.weapon.desc;
               if (wDesc) {
                   return wDesc[lang] || wDesc.pt || wDesc.en || ;
               }
           }
           // Para descrição base: aceita tanto desc_i18n quanto desc
           const base = s.desc_i18n || s.desc;
           if (base) {
               return base[lang] || base.pt || base.en || ;
           }
           // Fallback para campos individuais (compatibilidade)
           const descI18n = {
               pt: s.descPt || ,
               en: s.descEn || ,
               es: s.descEs || ,
               pl: s.descPl || 
           };
           return descI18n[lang] || descI18n.pt || ;
       }
       // Retorna o vídeo correto (weapon ou normal)
       function getEffectiveVideo(s) {
           const weaponOn = isWeaponModeOn();
           if (weaponOn && s.weapon && s.weapon.video && s.weapon.video.trim() !== ) {
               return s.weapon.video;
           }
           return s.video || ;
       }
       function renderSubAttrs(s, L) {

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

${label}${val}

` : );

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

return rows.length ? `

${rows.join()}

` : ;

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

return `

${items}

`;

       }
       function applyFlagTooltips(container) {
           const skillsRoot = document.getElementById('skills');
           if (!skillsRoot) return;
           let pack = {};
           try { pack = JSON.parse(skillsRoot.dataset.i18nFlags || '{}'); } catch (e) { }
           const raw = (document.documentElement.lang || 'pt').toLowerCase();
           const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
           const dict = pack[lang] || pack.pt || {};
           const flags = container.querySelectorAll('.skill-flags .skill-flag[data-flag]');
           const tooltip = window.__globalSkillTooltip;
           if (!tooltip) return;
           flags.forEach(el => {
               const key = el.getAttribute('data-flag');
               const tip = (dict && dict[key]) || ;
               if (!tip) return;
               if (el.dataset.flagTipWired) return;
               el.dataset.flagTipWired = '1';
               el.setAttribute('aria-label', tip);
               if (el.hasAttribute('title')) el.removeAttribute('title');
               el.addEventListener('mouseenter', () => {
                   const tipEl = document.querySelector('.skill-tooltip');
                   if (tipEl) tipEl.classList.add('flag-tooltip');
                   tooltip.show(el, tip);
               });
               el.addEventListener('mousemove', () => {
                   if (performance.now() >= tooltip.lockUntil.value) {
                       tooltip.measureAndPos(el);
                   }
               });
               el.addEventListener('click', () => {
                   tooltip.lockUntil.value = performance.now() + 240;
                   tooltip.measureAndPos(el);
               });
               el.addEventListener('mouseleave', () => {
                   const tipEl = document.querySelector('.skill-tooltip');
                   if (tipEl) tipEl.classList.remove('flag-tooltip');
                   tooltip.hide();
               });
           });
       }
       function ensureRail(iconsBar) {
           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;
       }
       // CORREÇÃO 1: Função melhorada para mostrar vídeo sem recarregar
       function showSubVideo(key, videoBox) {
           logSubVideo('showSubVideo called', { key, videoBoxExists: !!videoBox });
           if (!videoBox) return;
           // Esconde todos os vídeos, sem pausar e sem resetar seu estado
           const allVideos = videoBox.querySelectorAll('video.skill-video');
           logSubVideo('hiding skill-video count', allVideos.length);
           allVideos.forEach(v => {
               v.style.display = 'none';
           });
           let v = null;
           // Primeiro tenta cache global (sincronizado com Character.Skills.html)
           if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) {
               v = window.__subskillVideosCache.get(key);
           }
           // Depois tenta cache local
           if (!v && subCache.has(key)) {
               v = subCache.get(key);
           }
           logSubVideo('showSubVideo resolved video element', {
               key,
               found: !!v,
               isConnected: v ? v.isConnected : false,
               parentNode: v ? (v.parentNode === videoBox) : false,
               fromGlobalCache: !!(window.__subskillVideosCache && window.__subskillVideosCache.has(key)),
               fromLocalCache: subCache.has(key)
           });
           if (!v) {
               logSubVideo('no video found for key, hiding box', key);
               videoBox.style.display = 'none';
               return;
           }
           // Verifica se o vídeo está no DOM, se não estiver, adiciona
           if (!v.isConnected || v.parentNode !== videoBox) {
               logSubVideo('video element not in DOM, re-adding', { key, isConnected: v.isConnected, parentNode: v.parentNode });
               if (v.parentNode) {
                   v.parentNode.removeChild(v);
               }
               videoBox.appendChild(v);
           }
           logSubVideo('about to show video', {
               key,
               readyState: v.readyState,
               paused: v.paused,
               currentTime: v.currentTime
           });
           // Mostra sem reiniciar vídeo
           videoBox.style.display = 'block';
           v.style.display = 'block';
           // Só roda play se o vídeo estiver parado e já tiver carregado
           if (v.paused && v.readyState >= 2) {
               v.play().then(() => {
                   logSubVideo('play() resolved', { key, currentTime: v.currentTime, readyState: v.readyState });
               }).catch(err => {
                   logSubVideo('play() rejected', { key, error: err });
               });
           }
       }
       // CORREÇÃO 2: Função melhorada para garantir cache de vídeos
       function ensureSubVideoCached(s, parentIdx, videoBox) {
           logSubVideo('ensureSubVideoCached called', {
               name: (s.name || s.n || ).trim(),
               parentIdx,
               hasVideo: !!(s.video && s.video.trim()),
               rawVideo: s.video
           });
           const key = `sub:${parentIdx}:${(s.name || s.n || ).trim()}`;
           // Se já existe no cache global, usa ele
           if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) {
               logSubVideo('global cache hit in ensureSubVideoCached', key);
               const precreated = window.__subskillVideosCache.get(key);
               // Verifica se o vídeo está no DOM, se não estiver, adiciona
               if (precreated && (!precreated.isConnected || precreated.parentNode !== videoBox)) {
                   logSubVideo('cached video not in DOM, re-adding', { key, isConnected: precreated.isConnected });
                   if (precreated.parentNode) {
                       precreated.parentNode.removeChild(precreated);
                   }
                   videoBox.appendChild(precreated);
               }
               if (!subCache.has(key)) {
                   subCache.set(key, precreated);
               }
               return key;
           }
           if (subCache.has(key)) {
               logSubVideo('local cache hit in ensureSubVideoCached', key);
               const cached = subCache.get(key);
               // Verifica se o vídeo está no DOM, se não estiver, adiciona
               if (cached && (!cached.isConnected || cached.parentNode !== videoBox)) {
                   logSubVideo('cached video not in DOM, re-adding', { key, isConnected: cached.isConnected });
                   if (cached.parentNode) {
                       cached.parentNode.removeChild(cached);
                   }
                   videoBox.appendChild(cached);
               }
               return key;
           }
           if (!s.video || s.video.trim() === ) return key;
           const videoFileName = s.video.trim();
           if (videoFileName === 'Nada.png' || videoFileName.toLowerCase().includes('nada.png')) {
               return key;
           }
           const videoURL = normalizeFileURL(videoFileName);
           if (!videoURL || videoURL.trim() === ) return key;
           logSubVideo('creating new base subskill video', { key, videoFileName, videoURL });
           const v = document.createElement('video');
           v.className = 'skill-video';
           v.dataset.sub = '1';
           v.dataset.cached = 'true';
           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' });
           // Detectar formato do vídeo pela extensão
           const ext = (videoURL.split('.').pop() || ).toLowerCase().split('?')[0];
           const mimeTypes = {
               'mp4': 'video/mp4',
               'm4v': 'video/mp4',
               'webm': 'video/webm',
               'ogv': 'video/ogg',
               'ogg': 'video/ogg',
               'mov': 'video/quicktime'
           };
           const mimeType = mimeTypes[ext] || 'video/mp4';
           const src = document.createElement('source');
           src.src = videoURL;
           src.type = mimeType;
           v.appendChild(src);
           // Fallback para Safari/iOS mais antigos
           v.setAttribute('webkit-playsinline', );
           v.setAttribute('x-webkit-airplay', 'allow');
           videoBox.appendChild(v);
           // Adiciona ao cache local e global
           subCache.set(key, v);
           if (window.__subskillVideosCache) {
               window.__subskillVideosCache.set(key, v);
           }
           // Carrega o vídeo apenas uma vez - GARANTE: só chame load() se readyState === 0
           if (v.readyState === 0) {
               logSubVideo('calling load() for base subskill video', { key, readyState: v.readyState });
               v.load();
           }
           return key;
       }
       // Função para criar vídeo de weapon em cache
       function ensureSubVideoInCache(videoFileName, key, videoBox) {
           logSubVideo('ensureSubVideoInCache called', { key, videoFileName });
           // Verifica ambos os caches ANTES de criar
           if (window.__subskillVideosCache && window.__subskillVideosCache.has(key)) {
               logSubVideo('global cache hit in ensureSubVideoInCache', key);
               const precreated = window.__subskillVideosCache.get(key);
               // Verifica se o vídeo está no DOM, se não estiver, adiciona
               if (precreated && (!precreated.isConnected || precreated.parentNode !== videoBox)) {
                   logSubVideo('cached weapon video not in DOM, re-adding', { key, isConnected: precreated.isConnected });
                   if (precreated.parentNode) {
                       precreated.parentNode.removeChild(precreated);
                   }
                   videoBox.appendChild(precreated);
               }
               if (!subCache.has(key)) {
                   subCache.set(key, precreated);
               }
               return key;
           }
           if (subCache.has(key)) {
               logSubVideo('local cache hit in ensureSubVideoInCache', key);
               const cached = subCache.get(key);
               // Verifica se o vídeo está no DOM, se não estiver, adiciona
               if (cached && (!cached.isConnected || cached.parentNode !== videoBox)) {
                   logSubVideo('cached weapon video not in DOM, re-adding', { key, isConnected: cached.isConnected });
                   if (cached.parentNode) {
                       cached.parentNode.removeChild(cached);
                   }
                   videoBox.appendChild(cached);
               }
               return key;
           }
           if (!videoFileName || videoFileName.trim() === ) return key;
           const videoFile = videoFileName.trim();
           if (videoFile === 'Nada.png' || videoFile.toLowerCase().includes('nada.png')) {
               return key;
           }
           const videoURL = normalizeFileURL(videoFile);
           if (!videoURL || videoURL.trim() === ) return key;
           logSubVideo('creating new weapon subskill video', { key, videoFile, videoURL });
           const v = document.createElement('video');
           v.className = 'skill-video';
           v.dataset.sub = '1';
           v.dataset.weapon = '1';
           v.dataset.cached = 'true';
           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 ext = (videoURL.split('.').pop() || ).toLowerCase().split('?')[0];
           const mimeTypes = {
               'mp4': 'video/mp4',
               'm4v': 'video/mp4',
               'webm': 'video/webm',
               'ogv': 'video/ogg',
               'ogg': 'video/ogg',
               'mov': 'video/quicktime'
           };
           const mimeType = mimeTypes[ext] || 'video/mp4';
           const src = document.createElement('source');
           src.src = videoURL;
           src.type = mimeType;
           v.appendChild(src);
           v.setAttribute('webkit-playsinline', );
           v.setAttribute('x-webkit-airplay', 'allow');
           videoBox.appendChild(v);
           // Adiciona ao cache local e global
           subCache.set(key, v);
           if (window.__subskillVideosCache) {
               window.__subskillVideosCache.set(key, v);
           }
           // Carrega apenas uma vez - GARANTE: só chame load() se readyState === 0
           if (v.readyState === 0) {
               logSubVideo('calling load() for weapon subskill video', { key, readyState: v.readyState });
               v.load();
           }
           return key;
       }
       api.refreshCurrentSubSafe = function () {
           const btn = document.querySelector('.subskills-rail .subicon.active');
           if (!btn) return false;
           const had = document.body.dataset.suppressSkillPlay;
           document.body.dataset.suppressSkillPlay = '1';
           try {
               btn.dispatchEvent(new Event('click', { bubbles: true }));
           } finally {
               if (had) document.body.dataset.suppressSkillPlay = had;
               else delete document.body.dataset.suppressSkillPlay;
           }
           return true;
       };
       // Função auxiliar para aplicar classes de weapon nas subskills renderizadas
       const applyWeaponClassesToSubskills = () => {
           const weaponOn = isWeaponModeOn();
           // Busca TODAS as subskills com weapon
           let weaponSubs = document.querySelectorAll('.subicon[data-weapon]');
           weaponSubs.forEach(el => {
               if (weaponOn) {
                   el.classList.add('has-weapon-available');
                   if (el.classList.contains('active')) {
                       el.classList.add('weapon-equipped');
                   }
               } else {
                   el.classList.remove('has-weapon-available');
                   el.classList.remove('weapon-equipped');
               }
           });
       };
       api.renderBarFrom = function (el, { iconsBar, descBox, videoBox }) {
           logSubVideo('api.renderBarFrom called', {
               hasEl: !!el,
               hasIconsBar: !!iconsBar,
               hasDescBox: !!descBox,
               hasVideoBox: !!videoBox,
               parentIndex: el?.dataset?.index || 'unknown'
           });
           const rail = ensureRail(iconsBar);
           if (!rail) {
               logSubVideo('api.renderBarFrom: no rail found, returning');
               return;
           }
           const rawSubs = el.getAttribute('data-subs') || ;
           const rawOrder = el.getAttribute('data-suborder') || ;
           const parentIdx = el.dataset.index || ;
           logSubVideo('api.renderBarFrom: parsing subs', {
               parentIdx,
               hasRawSubs: !!rawSubs.trim(),
               rawSubsLength: rawSubs.length
           });
           if (!rawSubs.trim()) {
               if (subRail) subRail.classList.add('collapsed');
               if (subRail) subRail.classList.add('hidden');
               if (subBar) subBar.innerHTML = ;
               if (spacer) spacer.style.height = '0px';
               return;
           }
           let subs;
           try { subs = JSON.parse(rawSubs); } catch { subs = []; }
           // NORMALIZADOR: Converte weaponPacked para weapon se necessário
           subs = subs.map(sub => {
               // Se tem weaponPacked mas não tem weapon, tenta parsear
               if (sub.weaponPacked && !sub.weapon && typeof sub.weaponPacked === 'string' && sub.weaponPacked.trim() !== ) {
                   const parts = sub.weaponPacked.split('~');
                   if (parts.length >= 2) {
                       sub.weapon = {
                           icon: parts[0] || 'Nada.png',
                           powerpve: parts[1] || null,
                           powerpvp: parts[2] || null,
                           cooldown: parts[3] || null,
                           video: parts[4] || ,
                           energy: parts[5] || null
                       };
                       // Remove valores vazios
                       Object.keys(sub.weapon).forEach(k => {
                           if (sub.weapon[k] ===  || sub.weapon[k] === null) {
                               delete sub.weapon[k];
                           }
                       });
                   }
               }
               // Garante que weapon seja objeto válido
               if (sub.weapon && typeof sub.weapon === 'string') {
                   // Tenta parsear como JSON primeiro
                   try {
                       sub.weapon = JSON.parse(sub.weapon);
                   } catch {
                       // Se falhar, tenta formato ~
                       const parts = sub.weapon.split('~');
                       if (parts.length >= 2) {
                           sub.weapon = {
                               icon: parts[0] || 'Nada.png',
                               powerpve: parts[1] || null,
                               powerpvp: parts[2] || null,
                               cooldown: parts[3] || null,
                               video: parts[4] || ,
                               energy: parts[5] || null
                           };
                           Object.keys(sub.weapon).forEach(k => {
                               if (sub.weapon[k] ===  || sub.weapon[k] === null) {
                                   delete sub.weapon[k];
                               }
                           });
                       } else {
                           sub.weapon = null;
                       }
                   }
               }
               return sub;
           });
           if (!Array.isArray(subs) || subs.length === 0) {
               subRail.classList.add('collapsed');
               subRail.classList.add('hidden');
               subBar.innerHTML = ;
               if (spacer) spacer.style.height = '0px';
               return;
           }
           // Busca mapa das skills principais para herança
           const mainSkills = getMainSkillsMap();
           // Aplica herança ANTES de processar
           subs = subs.map(sub => applyInheritance(sub, mainSkills));
           // Remove subskills que ficaram sem nome após herança
           subs = subs.filter(s => (s.name || s.n || ).trim() !== );
           subRail.classList.add('hidden');
           subBar.innerHTML = ;
           // Usa a ordem natural das subskills após herança
           let order = subs.map(s => s.name || s.n || );
           if (rawOrder.trim()) {
               try {
                   const preferred = JSON.parse(rawOrder);
                   if (Array.isArray(preferred) && preferred.length) {
                       const byName = new Map(subs.map(s => [(s.name || s.n || ), s]));
                       order = preferred.filter(n => byName.has(n));
                   }
               } catch { }
           }
           // CORREÇÃO 3: Pré-criar todos os vídeos ANTES de renderizar os ícones
           logSubVideo('pre-creating all subskill videos before render', { parentIdx, orderCount: order.length });
           order.forEach(nm => {
               const s = subs.find(x => (x.name || x.n || ) === nm);
               if (s) {
                   // Pré-cria vídeo normal
                   if (s.video) {
                       logSubVideo('pre-creating base video in renderBarFrom', { subName: (s.name || s.n || ).trim(), parentIdx });
                       ensureSubVideoCached(s, parentIdx, videoBox);
                   }
                   // Pré-cria vídeo de weapon se existir
                   if (s.weapon && s.weapon.video) {
                       const weaponKey = `sub:${parentIdx}:${(s.name || s.n || ).trim()}:weapon`;
                       logSubVideo('pre-creating weapon video in renderBarFrom', { subName: (s.name || s.n || ).trim(), parentIdx, weaponKey });
                       ensureSubVideoInCache(s.weapon.video, weaponKey, videoBox);
                   }
                   if (s.icon) preloadImage(s.icon);
               }
           });
           order.forEach(nm => {
               const s = subs.find(x => (x.name || x.n || ) === nm);
               if (!s) {
                   return;
               }
               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);
               logSubVideo('creating subicon element', {
                   subName: (s.name || s.n || ).trim(),
                   parentIdx,
                   className: item.className
               });
               const img = document.createElement('img');
               img.alt = ;
               img.src = filePathURL(s.icon || 'Nada.png');
               item.appendChild(img);
               // Verifica weapon de forma mais robusta
               const hasWeapon = s.weapon && (
                   (typeof s.weapon === 'object' && Object.keys(s.weapon).length > 0) ||
                   (typeof s.weapon === 'string' && s.weapon.trim() !== )
               );
               const subName = (s.name || s.n || ).trim();
               if (hasWeapon) {
                   // Normaliza weapon se for string
                   let weaponObj = s.weapon;
                   if (typeof weaponObj === 'string') {
                       try {
                           weaponObj = JSON.parse(weaponObj);
                       } catch {
                           // Se falhar, tenta formato ~
                           const parts = weaponObj.split('~');
                           if (parts.length >= 2) {
                               weaponObj = {
                                   icon: parts[0] || 'Nada.png',
                                   powerpve: parts[1] || null,
                                   powerpvp: parts[2] || null,
                                   cooldown: parts[3] || null,
                                   video: parts[4] || ,
                                   energy: parts[5] || null
                               };
                               Object.keys(weaponObj).forEach(k => {
                                   if (weaponObj[k] ===  || weaponObj[k] === null) {
                                       delete weaponObj[k];
                                   }
                               });
                           } else {
                               weaponObj = null;
                           }
                       }
                   }
                   if (weaponObj && typeof weaponObj === 'object' && Object.keys(weaponObj).length > 0) {
                       try {
                           item.dataset.weapon = JSON.stringify(weaponObj);
                       } catch (e) {
                           console.error('[Subskills] Erro ao serializar weapon de subskill', subName, e);
                       }
                       // Aplica classe inicial se o toggle já está ativo
                       if (isWeaponModeOn()) {
                           item.classList.add('has-weapon-available');
                       }
                   }
               }
               // CORREÇÃO 4: Evento de clique otimizado para não recarregar vídeos
               logSubVideo('attaching click handler to subicon', {
                   subName: (s.name || s.n || ).trim(),
                   parentIdx,
                   itemClassName: item.className
               });
               item.addEventListener('click', () => {
                   const L = getLabels();
                   const subName = (s.name || s.n || ).trim();
                   logSubVideo('subicon click HANDLER EXECUTED', {
                       subName,
                       parentIdx,
                       weaponMode: isWeaponModeOn(),
                       hasWeaponDataAttr: !!item.dataset.weapon,
                       rawWeaponDataset: item.dataset.weapon || null
                   });
                   // Lê weapon diretamente do atributo data-weapon
                   let subWeaponData = null;
                   if (item.dataset.weapon) {
                       try {
                           subWeaponData = JSON.parse(item.dataset.weapon);
                       } catch (e) {
                           subWeaponData = null;
                       }
                   }
                   const hasSubWeapon = !!subWeaponData;
                   const weaponOn = isWeaponModeOn();
                   const weaponEquipped = weaponOn && hasSubWeapon && subWeaponData;
                   logSubVideo('weapon status for subclick', {
                       subName,
                       weaponEquipped,
                       hasSubWeaponData: !!subWeaponData
                   });
                   // Determina descrição (weapon ou normal)
                   const raw = (document.documentElement.lang || 'pt').toLowerCase();
                   const lang = raw === 'pt-br' ? 'pt' : (raw.split('-')[0] || 'pt');
                   let chosen = ;
                   if (weaponEquipped && subWeaponData) {
                       // Descrição do weapon
                       const weaponDescPack = subWeaponData.desc_i18n || subWeaponData.desc || {};
                       chosen = weaponDescPack[lang] || weaponDescPack.pt || weaponDescPack.en || ;
                   } else {
                       // Descrição normal
                       const base = s.desc_i18n || s.desc || {};
                       chosen = base[lang] || base.pt || base.en || (s.descPt || );
                   }
                   // Determina atributos (weapon ou normal)
                   let attrsObj = {
                       powerpve: s.powerpve,
                       powerpvp: s.powerpvp,
                       energy: s.energy,
                       cooldown: s.cooldown
                   };
                   if (weaponEquipped && subWeaponData) {
                       attrsObj = {
                           powerpve: subWeaponData.powerpve || s.powerpve,
                           powerpvp: subWeaponData.powerpvp || s.powerpvp,
                           energy: subWeaponData.energy || s.energy,
                           cooldown: subWeaponData.cooldown || s.cooldown
                       };
                   }
                   // Level (weapon ou normal)
                   const level = (weaponEquipped && subWeaponData && subWeaponData.level)
                       ? subWeaponData.level.toString().trim()
                       : (s.level || ).toString().trim();
                   let flagsHTML = ;
                   if (Array.isArray(s.flags) && s.flags.length > 0) {
                       flagsHTML = renderFlagsRow(s.flags);
                   }
                   if (descBox) {

descBox.innerHTML = `

${s.name || nm}

${level ? `

${L.level} ${level}

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

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

`;

                   }
                   if (videoBox) {
                       const oldFlags = videoBox.querySelector('.skill-flags');
                       if (oldFlags) oldFlags.remove();
                       if (flagsHTML) {
                           videoBox.insertAdjacentHTML('beforeend', flagsHTML);
                           applyFlagTooltips(videoBox);
                       }
                   }
                   // Vídeo (weapon ou normal) - usa cache já criado
                   const effectiveVideo = getEffectiveVideo(s);
                   logSubVideo('effectiveVideo for sub', {
                       subName,
                       parentIdx,
                       effectiveVideo,
                       weaponEquipped,
                       rawBaseVideo: s.video,
                       rawWeaponVideo: subWeaponData?.video
                   });
                   if (!effectiveVideo || effectiveVideo.trim() ===  || effectiveVideo === 'Nada.png' || effectiveVideo.toLowerCase().includes('nada.png')) {
                       if (videoBox) videoBox.style.display = 'none';
                   } else {
                       const videoKey = (weaponEquipped && subWeaponData && subWeaponData.video && subWeaponData.video.trim() !== )
                           ? `sub:${parentIdx}:${subName}:weapon`
                           : `sub:${parentIdx}:${subName}`;
                       logSubVideo('computed videoKey for subclick', { videoKey });
                       logSubVideo('calling showSubVideo from click handler', { videoKey });
                       // NÃO cria novo vídeo - apenas mostra o que já está em cache
                       showSubVideo(videoKey, videoBox);
                   }
                   Array.from(subBar.children).forEach(c => {
                       c.classList.remove('active');
                       c.classList.remove('weapon-equipped');
                   });
                   item.classList.add('active');
                   // Aplica weapon-equipped se tem weapon e está ativo
                   if (weaponEquipped) {
                       item.classList.add('weapon-equipped');
                   }
                   window.__lastActiveSkillIcon = item;
               });
               if (window.__globalSkillTooltip) {
                   const { show, hide, measureAndPos, lockUntil } = window.__globalSkillTooltip;
                   const label = item.title || ;
                   item.setAttribute('aria-label', label);
                   if (item.hasAttribute('title')) item.removeAttribute('title');
                   item.addEventListener('mouseenter', () => show(item, label));
                   item.addEventListener('mousemove', () => { if (performance.now() >= lockUntil.value) measureAndPos(item); });
                   item.addEventListener('click', () => { lockUntil.value = performance.now() + 240; measureAndPos(item); });
                   item.addEventListener('mouseleave', hide);
               }
               subBar.appendChild(item);
               logSubVideo('subicon appended to subBar', {
                   subName: (s.name || s.n || ).trim(),
                   parentIdx,
                   subBarChildrenCount: subBar.children.length
               });
           });
           // Aplica classes de weapon nas subskills recém-renderizadas
           setTimeout(() => {
               applyWeaponClassesToSubskills();
               // Dispara evento para notificar que subskills estão prontas
               window.dispatchEvent(new CustomEvent('gla:subskills:ready', { detail: { count: order.length } }));
               // Remove listener anterior se existir (evita duplicação)
               if (subBar._weaponToggleListener) {
                   window.removeEventListener('gla:weaponToggled', subBar._weaponToggleListener);
               }
               // Cria novo listener que opera no subBar atual
               subBar._weaponToggleListener = (e) => {
                   const enabled = e.detail?.enabled ?? false;
                   // Aplica classes usando a função auxiliar
                   applyWeaponClassesToSubskills();
                   // Atualiza a subskill ativa se houver
                   setTimeout(() => {
                       const activeSub = subBar.querySelector('.subicon[data-weapon].active')
                           || subBar.querySelector('.subicon.active');
                       if (activeSub) {
                           activeSub.dispatchEvent(new Event('click', { bubbles: true }));
                       } else {
                           api.refreshCurrentSubSafe();
                       }
                   }, 50);
               };
               window.addEventListener('gla:weaponToggled', subBar._weaponToggleListener);
               requestAnimationFrame(() => {
                   subRail.classList.remove('collapsed');
                   subRail.classList.remove('hidden');
                   const h = subRail.offsetHeight || 48;
                   if (spacer) spacer.style.height = h + 'px';
               });
           }, 10);
       };
       api.hideAll = function (videoBox) {
           const videos = videoBox?.querySelectorAll('.skill-video[data-sub="1"]') || [];
           logSubVideo('api.hideAll called', { videoBoxExists: !!videoBox, videoCount: videos.length });
           videos.forEach(v => {
               try { v.pause(); } catch { }
               v.style.display = 'none';
           });
       };
       window.renderSubskillsBarFrom = function (el, ctx) { api.renderBarFrom(el, ctx); };
       api.preloadAllSubskillImages = function () {
           const allSkillIcons = document.querySelectorAll('.icon-bar .skill-icon[data-subs]');
           const preloadPromises = [];
           let totalImages = 0;
           allSkillIcons.forEach(icon => {
               try {
                   const subsRaw = icon.getAttribute('data-subs');
                   if (!subsRaw) return;
                   const subs = JSON.parse(subsRaw);
                   if (!Array.isArray(subs)) return;
                   subs.forEach(s => {
                       if (s && s.icon) {
                           preloadPromises.push(preloadImage(s.icon));
                           totalImages++;
                       }
                       if (s && Array.isArray(s.subs)) {
                           s.subs.forEach(nested => {
                               if (nested && nested.icon) {
                                   preloadPromises.push(preloadImage(nested.icon));
                                   totalImages++;
                               }
                           });
                       }
                   });
               } catch (e) {
               }
           });
           if (totalImages > 0) {
               return Promise.all(preloadPromises).then(() => {
               });
           }
           return Promise.resolve();
       };
       // CORREÇÃO 5: Nova função para pré-carregar vídeos de subskills
       api.preloadAllSubskillVideos = function () {
           const allSkillIcons = document.querySelectorAll('.icon-bar .skill-icon[data-subs]');
           allSkillIcons.forEach(icon => {
               try {
                   const subsRaw = icon.getAttribute('data-subs');
                   if (!subsRaw) return;
                   const subs = JSON.parse(subsRaw);
                   if (!Array.isArray(subs)) return;
                   const parentIdx = icon.dataset.index || ;
                   logSubVideo('preloadAllSubskillVideos entry', {
                       parentIdx,
                       hasSubs: Array.isArray(subs),
                       subsCount: Array.isArray(subs) ? subs.length : 0
                   });
                   subs.forEach(s => {
                       // Pré-carrega vídeo normal
                       if (s.video && s.video.trim() !==  && s.video !== 'Nada.png' && !s.video.toLowerCase().includes('nada.png')) {
                           logSubVideo('preloading base subskill video (link preload)', {
                               parentIdx,
                               subName: (s.name || s.n || ).trim(),
                               video: s.video
                           });
                           const videoURL = normalizeFileURL(s.video);
                           if (videoURL) {
                               // Pré-carregar via link preload
                               const link = document.createElement('link');
                               link.rel = 'preload';
                               link.as = 'video';
                               link.href = videoURL;
                               link.crossOrigin = 'anonymous';
                               document.head.appendChild(link);
                           }
                       }
                       // Pré-carrega vídeo de weapon
                       if (s.weapon && typeof s.weapon === 'object' && s.weapon.video && s.weapon.video.trim() !==  && s.weapon.video !== 'Nada.png' && !s.weapon.video.toLowerCase().includes('nada.png')) {
                           logSubVideo('preloading weapon subskill video (link preload)', {
                               parentIdx,
                               subName: (s.name || s.n || ).trim(),
                               weaponVideo: s.weapon.video
                           });
                           const weaponVideoURL = normalizeFileURL(s.weapon.video);
                           if (weaponVideoURL) {
                               const link = document.createElement('link');
                               link.rel = 'preload';
                               link.as = 'video';
                               link.href = weaponVideoURL;
                               link.crossOrigin = 'anonymous';
                               document.head.appendChild(link);
                           }
                       }
                   });
               } catch (e) {
               }
           });
       };
       // CORREÇÃO 6: Função para sincronizar cache de vídeos
       function syncVideoCaches() {
           if (!window.__subskillVideosCache) {
               window.__subskillVideosCache = new Map();
           }
           // Copiar vídeos do cache local para o global
           subCache.forEach((video, key) => {
               if (!window.__subskillVideosCache.has(key)) {
                   window.__subskillVideosCache.set(key, video);
               }
           });
           // Copiar vídeos do global para o local
           if (window.__subskillVideosCache) {
               window.__subskillVideosCache.forEach((video, key) => {
                   if (!subCache.has(key)) {
                       subCache.set(key, video);
                   }
               });
           }
           logSubVideo('syncVideoCaches', {
               localSize: subCache.size,
               globalSize: window.__subskillVideosCache ? window.__subskillVideosCache.size : 0
           });
       }
       // Inicialização
       function init() {
           // Constrói cache das skills principais
           getMainSkillsMap();
           // Pré-carrega imagens das subskills
           api.preloadAllSubskillImages();
           // CORREÇÃO 7: Pré-carrega vídeos das subskills
           api.preloadAllSubskillVideos();
           // Sincroniza caches periodicamente
           setInterval(syncVideoCaches, 2000);
           // Escuta mudanças no localStorage
           window.addEventListener('storage', (e) => {
               if (e.key === 'glaWeaponEnabled') {
                   setTimeout(() => api.refreshCurrentSubSafe(), 50);
               }
           });
           // LISTENER GLOBAL: Escuta evento de toggle
           window.addEventListener('gla:weaponToggled', (e) => {
               const enabled = e.detail?.enabled ?? false;
               // Tenta aplicar classes imediatamente
               applyWeaponClassesToSubskills();
               // Atualiza a subskill ativa se houver
               setTimeout(() => {
                   const activeSub = document.querySelector('.subicon[data-weapon].active')
                       || document.querySelector('.subicon.active');
                   if (activeSub) {
                       activeSub.dispatchEvent(new Event('click', { bubbles: true }));
                   } else {
                       api.refreshCurrentSubSafe();
                   }
               }, 50);
           });
           // LISTENER: Escuta quando subskills estão prontas
           window.addEventListener('gla:subskills:ready', (e) => {
               // Aplica classes de weapon se o toggle estiver ativo
               applyWeaponClassesToSubskills();
           });
       }
       if (document.readyState === 'loading') {
           document.addEventListener('DOMContentLoaded', () => {
               setTimeout(init, 100);
           });
       } else {
           setTimeout(init, 100);
       }
   })();

</script> <style>

   .subicon-bar {
       display: flex;
       gap: 10px;
       padding: 6px 6px;
       overflow-x: auto;
       /* Firefox */
       scrollbar-width: thin;
       scrollbar-color: #ababab transparent;
   }
   .subicon-bar::-webkit-scrollbar {
       height: 6px;
   }
   .subicon-bar::-webkit-scrollbar-thumb {
       background: #151515;
       border-radius: 3px;
   }
   .subicon {
       width: var(--icon-size, 42px);
       height: var(--icon-size, 42px);
       border-radius: var(--icon-radius, 10px);
       overflow: hidden;
       position: relative;
       flex: 0 0 auto;
       cursor: pointer;
       isolation: isolate;
   }
   .subicon img {
       width: 100%;
       height: 100%;
       object-fit: cover;
       display: block;
       border-radius: inherit;
   }
   .subicon::after {
       content: "";
       position: absolute;
       inset: 0;
       border-radius: inherit;
       box-shadow: inset 0 0 0 var(--icon-ring-w, 2px) var(--icon-idle, #cfcfcf);
       pointer-events: none;
       z-index: 2;
       transition: box-shadow .12s ease;
   }
   .subicon:hover::after {
       box-shadow: inset 0 0 0 var(--icon-ring-w, 2px) #e6e6e6;
   }
   .subicon.active::after {
       box-shadow: inset 0 0 0 var(--icon-ring-w, 2px) var(--icon-active, #FFD95A);
   }
   .subicon.active::before {
       content: "";
       position: absolute;
       inset: -4px;
       border-radius: calc(var(--icon-radius, 10px) + 4px);
       pointer-events: none;
       z-index: 1;
       opacity: 1;
       box-shadow: 0 0 12px 3px var(--icon-active-glow, rgba(255, 217, 90, .30)),
           0 0 0 calc(var(--icon-ring-w, 2px) * 2) var(--icon-active-ring, rgba(255, 217, 90, .50));
   }
   .top-rail.skills {
       position: relative;
       display: flex;
       flex-direction: column;
       align-items: center;
       overflow: visible;
   }
   .top-rail.skills .icon-bar {
       margin-bottom: 0;
       position: relative;
       z-index: 2;
   }
   .subskills-rail {
       position: absolute;
       left: 50%;
       transform: translateX(-50%);
       top: calc(100% - 1px);
       z-index: 3;
       display: inline-flex;
       justify-content: center;
       align-items: center;
       width: auto;
       max-width: 100%;
       padding: 3px 5px;
       background: rgba(0, 0, 0, .38);
       border: 1px solid rgba(255, 255, 255, .10);
       border-top: 1px solid rgba(255, 255, 255, .08);
       border-radius: 0 0 10px 10px;
       box-shadow: 0 3px 9px rgba(0, 0, 0, .22);
       -webkit-backdrop-filter: blur(2px);
       backdrop-filter: blur(2px);
       overflow: hidden;
       transition: opacity .14s ease, transform .14s ease;
       opacity: 1;
   }
   .subskills-rail::before {
       content: "";
       position: absolute;
       top: -6px;
       left: 0;
       right: 0;
       height: 6px;
       background: linear-gradient(to bottom, rgba(0, 0, 0, .20), rgba(0, 0, 0, 0));
       pointer-events: none;
   }
   .subskills-rail.collapsed {
       opacity: 0;
       pointer-events: none;
       transform: translate(-50%, -6px);
   }
   .subskills-rail.hidden {
       visibility: hidden;
   }
   .subskills-spacer {
       height: 0;
       transition: height .2s ease;
   }
   .subskills-rail .subicon-bar {
       display: inline-flex;
       align-items: center;
       gap: 0;
       overflow-x: auto;
       /* Firefox */
       scrollbar-width: thin;
       scrollbar-color: #ababab transparent;
   }
   .subskills-rail .subicon-bar::-webkit-scrollbar {
       height: 6px;
   }
   .subskills-rail .subicon-bar::-webkit-scrollbar-thumb {
       background: #151515;
       border-radius: 3px;
   }
   .subskills-rail .subicon {
       width: 42px;
       height: 42px;
       border-radius: 6px;
       position: relative;
       overflow: hidden;
       flex: 0 0 auto;
       cursor: pointer;
       isolation: isolate;
       -webkit-backface-visibility: hidden;
       backface-visibility: hidden;
       transform: translateZ(0);
   }
   .subskills-rail .subicon+.subicon {
       margin-left: 4px;
   }
   .subskills-rail .subicon img {
       width: 100%;
       height: 100%;
       object-fit: cover;
       display: block;
       border-radius: inherit;
   }
   .subskills-rail .subicon::after {
       content: "";
       position: absolute;
       inset: 0;
       border-radius: inherit;
       box-shadow: inset 0 0 0 2px var(--icon-idle, #cfcfcf);
       pointer-events: none;
       z-index: 2;
       transition: box-shadow .12s ease;
   }
   .subskills-rail .subicon:hover::after {
       box-shadow: inset 0 0 0 2px #e6e6e6;
   }
   .subskills-rail .subicon.active::after {
       box-shadow: inset 0 0 0 2px var(--icon-active, #FFD95A);
   }
   .video-container .skill-video {
       width: 100%;
       height: auto;
       aspect-ratio: 16 / 9;
       object-fit: cover;
       background: #000;
       border-radius: 10px;
   }
   @media (max-width: 900px) {
       .subskills-rail {
           position: static;
           transform: none;
           margin-top: -2px;
           border-top: 0;
           border-radius: 0 0 10px 10px;
       }
       .subskills-spacer {
           height: 0 !important;
       }
   }
   .skills-rail-wrap {
       position: relative;
       display: block;
       width: max-content;
       margin: 0 auto;
   }
   /* Subskills com arma disponível - borda vermelha quando inativa */
   .subicon.has-weapon-available:not(.active)::after {
       box-shadow: inset 0 0 0 2px rgba(220, 70, 70, 0.8) !important;
   }
   /* Subskill com arma ATIVA - laranja/coral vibrante */
   .subicon.has-weapon-available.active::after {
       box-shadow: inset 0 0 0 2px #FF7043 !important;
   }
   .subicon.has-weapon-available.active::before {
       box-shadow: 0 0 12px 3px rgba(255, 112, 67, 0.35), 0 0 0 4px rgba(255, 112, 67, 0.25) !important;
   }
   /* Modo arma ON - efeito animado nos subicons */
   .top-rail.skills.weapon-mode-on .subicon.has-weapon-available {
       position: relative;
   }
   .top-rail.skills.weapon-mode-on .subicon.has-weapon-available::after {
       box-shadow: none !important;
       background: linear-gradient(90deg,
               rgba(255, 80, 80, 0.9) 0%,
               rgba(255, 120, 60, 1) 25%,
               rgba(255, 80, 80, 0.9) 50%,
               rgba(255, 120, 60, 1) 75%,
               rgba(255, 80, 80, 0.9) 100%) !important;
       background-size: 400% 100% !important;
       animation: weapon-subicon-border-scan 4s linear infinite !important;
       -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0) !important;
       mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0) !important;
       -webkit-mask-composite: xor !important;
       mask-composite: exclude !important;
       padding: 2px !important;
   }
   .top-rail.skills.weapon-mode-on .subicon.has-weapon-available::before {
       content: "";
       position: absolute;
       inset: -4px;
       border-radius: calc(6px + 4px);
       pointer-events: none;
       z-index: 1;
       animation: weapon-subicon-pulse 3s ease-in-out infinite !important;
   }
   /* Subskill ativa com arma - mais intenso */
   .top-rail.skills.weapon-mode-on .subicon.has-weapon-available.active::after {
       background: linear-gradient(90deg,
               rgba(255, 87, 34, 1) 0%,
               rgba(255, 140, 60, 1) 25%,
               rgba(255, 87, 34, 1) 50%,
               rgba(255, 140, 60, 1) 75%,
               rgba(255, 87, 34, 1) 100%) !important;
   }
   .top-rail.skills.weapon-mode-on .subicon.has-weapon-available.active::before {
       box-shadow: 0 0 14px 4px rgba(255, 87, 34, 0.4), 0 0 0 4px rgba(255, 87, 34, 0.3) !important;
       animation: weapon-subicon-pulse 2.5s ease-in-out infinite !important;
   }
   @keyframes weapon-subicon-border-scan {
       0% {
           background-position: 0% 0%;
       }
       100% {
           background-position: 400% 0%;
       }
   }
   @keyframes weapon-subicon-pulse {
       0%,
       100% {
           opacity: 0.5;
           box-shadow: 0 0 8px rgba(255, 80, 80, 0.25);
       }
       50% {
           opacity: 1;
           box-shadow: 0 0 16px rgba(255, 80, 80, 0.45);
       }
   }

</style>