Mudanças entre as edições de "Widget:C.Skills"
Ir para navegação
Ir para pesquisar
m |
m |
||
| Linha 1 978: | Linha 1 978: | ||
} | } | ||
.character-box .top-rail.skills .icon-bar .skill-icon.effect-active:not(.weapon-bar-toggle):not(.active)::after { | .character-box .top-rail.skills .icon-bar .skill-icon.effect-active:not(.weapon-bar-toggle):not(.active)::after { | ||
box-shadow: inset 0 0 0 var(--icon-ring-w) rgba( | box-shadow: | ||
inset 0 0 0 var(--icon-ring-w) rgba(210, 60, 60, 0.95), | |||
inset 0 0 0 calc(var(--icon-ring-w) + 1px) rgba(120, 20, 20, 0.75) !important; | |||
animation: effect-child-ring 1.6s ease-in-out infinite !important; | animation: effect-child-ring 1.6s ease-in-out infinite !important; | ||
} | } | ||
@keyframes effect-child-ring { | @keyframes effect-child-ring { | ||
0% { | 0% { | ||
box-shadow: inset 0 0 0 var(--icon-ring-w) rgba( | box-shadow: | ||
inset 0 0 0 var(--icon-ring-w) rgba(210, 60, 60, 0.95), | |||
inset 0 0 0 calc(var(--icon-ring-w) + 1px) rgba(120, 20, 20, 0.6); | |||
} | } | ||
50% { | 50% { | ||
box-shadow: inset 0 0 0 var(--icon-ring-w) rgba( | box-shadow: | ||
inset 0 0 0 var(--icon-ring-w) rgba(120, 20, 20, 0.95), | |||
inset 0 0 0 calc(var(--icon-ring-w) + 1px) rgba(20, 0, 0, 0.95); | |||
} | } | ||
100% { | 100% { | ||
box-shadow: inset 0 0 0 var(--icon-ring-w) rgba( | box-shadow: | ||
inset 0 0 0 var(--icon-ring-w) rgba(210, 60, 60, 0.95), | |||
inset 0 0 0 calc(var(--icon-ring-w) + 1px) rgba(120, 20, 20, 0.6); | |||
} | } | ||
} | } | ||
Edição das 00h57min de 25 de janeiro de 2026
<script>
(function () {
const $ = (s, root = document) => root.querySelector(s);
const $$ = (s, root = document) => Array.from(root.querySelectorAll(s));
const ensureRemoved = (sel) => {
Array.from(document.querySelectorAll(sel)).forEach((n) => n.remove());
};
const onceFlag = (el, key) => {
if (!el) return false;
if (el.dataset[key]) return false;
el.dataset[key] = "1";
return true;
};
const addOnce = (el, ev, fn, options = {}) => {
if (!el) return;
const attr = `data-wired-${ev}`;
if (el.hasAttribute(attr)) return;
el.addEventListener(ev, fn, options);
el.setAttribute(attr, "1");
};
const FLAG_ICON_FILES = {
aggro: "Enemyaggro-icon.png",
bridge: "Bridgemaker-icon.png",
wall: "Destroywall-icon.png",
quickcast: "Quickcast-icon.png",
wallpass: "Passthroughwall-icon.png",
};
const subBarTemplateCache =
window.__skillSubBarTemplateCache ||
(window.__skillSubBarTemplateCache = new Map());
const imagePreloadCache =
window.__skillImagePreloadCache ||
(window.__skillImagePreloadCache = new Map());
const videoPreloadCache =
window.__skillVideoPreloadCache ||
(window.__skillVideoPreloadCache = new Set());
const flagRowCache =
window.__skillFlagRowCache || (window.__skillFlagRowCache = new Map());
const flagIconURLCache =
window.__skillFlagIconURLCache ||
(window.__skillFlagIconURLCache = new Map());
// Sistema de múltiplas formas (genérico)
let currentForm = null; // null = primeira forma, depois nome da forma atual
let formsData = {};
let fixedSkills = []; // Skills que sempre aparecem (Change Form, Guard Point, etc.)
// Sistema genérico de swap de personagens (ex: Buchi & Sham, futuros personagens) let activeCharacter = null; // null = personagem padrão, depois nome do personagem ativo (ex: "Buchi", "Sham")
function showFormTransitionVideo(changeFormIconEl) {
const skillsRoot = document.getElementById("skills");
if (!skillsRoot) return;
// Busca videoBox
let videoBox = skillsRoot.querySelector(".video-container");
if (!videoBox) {
const skillsContainer = skillsRoot.querySelector(".skills-container");
if (skillsContainer) {
videoBox = skillsContainer.querySelector(".video-container");
}
}
if (!videoBox) return;
try {
// Lê dados de forms para determinar forma atual e próxima
const formsJSON = skillsRoot.dataset.forms || "{}";
if (formsJSON && formsJSON !== "{}") {
const tempFormsData = JSON.parse(formsJSON);
const formNames = Object.keys(tempFormsData);
// Determina forma atual e próxima
const currentIdx = currentForm ? formNames.indexOf(currentForm) : -1;
const nextIdx = (currentIdx + 1) % formNames.length;
const nextForm = formNames[nextIdx];
// Busca vídeo de transição na skill Change Form
// form_videos[forma_atual] = "video.mp4" (vídeo da transição atual → próxima)
const formVideosRaw =
changeFormIconEl.dataset.formVideos ||
changeFormIconEl.getAttribute("data-form-videos");
if (formVideosRaw) {
try {
const videos = JSON.parse(formVideosRaw);
const transitionVideo = videos[currentForm] || "";
if (transitionVideo && transitionVideo.trim() !== "") {
const videoURL = filePathURL(transitionVideo);
if (videoURL) {
// Busca ou cria elemento de vídeo para esta transição
const videoKey = `form_transition:${currentForm}:${nextForm}`;
let v = videosCache.get(videoKey);
if (!v) {
// Cria novo elemento de vídeo
v = document.createElement("video");
v.className = "skill-video";
v.src = videoURL;
v.preload = "auto";
v.controls = false;
v.muted = false;
v.loop = false;
v.playsInline = true;
videoBox.appendChild(v);
videosCache.set(videoKey, v);
}
// Mostra e reproduz o vídeo
Array.from(
videoBox.querySelectorAll("video.skill-video")
).forEach((vid) => {
try {
vid.pause();
} catch (e) { }
vid.style.display = "none";
});
videoBox.style.display = "block";
v.style.display = "block";
try {
v.currentTime = 0;
v.play().catch(() => { });
} catch (e) { }
}
}
} catch (e) {
console.error("[Forms] Erro ao parsear form_videos:", e);
}
}
}
} catch (e) {
console.error("[Forms] Erro ao processar vídeo de transição:", e);
}
}
// Sistema genérico de swap de personagens (ex: Buchi & Sham, futuros personagens)
// Detecta quais personagens estão disponíveis baseado nas skills
function detectAvailableCharacters() {
const iconBar = document.querySelector(".icon-bar");
if (!iconBar) return [];
const characters = new Set();
Array.from(
iconBar.querySelectorAll(".skill-icon[data-only-character]")
).forEach((icon) => {
const character = (icon.dataset.onlyCharacter || "").trim();
if (character) {
characters.add(character);
}
});
return Array.from(characters); }
// Inicializa o personagem padrão (deve ser chamado quando a página carrega)
function initializeActiveCharacter() {
const availableCharacters = detectAvailableCharacters();
if (availableCharacters.length > 0 && activeCharacter === null) {
// Inicializa com o primeiro personagem disponível (padrão)
activeCharacter = availableCharacters[0];
// Aplica o estado inicial (habilita/desabilita skills)
applyCharacterSwapState();
}
}
// Aplica o estado de swap nas skills (habilita/desabilita e atualiza vídeos)
function applyCharacterSwapState() {
const iconBar = document.querySelector(".icon-bar");
if (!iconBar) return;
// Atualiza todas as skills na barra
Array.from(iconBar.querySelectorAll(".skill-icon[data-index]")).forEach(
(icon) => {
const onlyCharacter = (icon.dataset.onlyCharacter || "").trim();
const hasCharacterVideos =
icon.dataset.characterVideos &&
icon.dataset.characterVideos.trim() !== "";
const baseVideoFile = icon.dataset.videoFile || "";
const baseVideoURL = icon.dataset.video || "";
// Salva o vídeo original se ainda não foi salvo (apenas para skills compartilhadas com character_videos)
if (
hasCharacterVideos &&
!onlyCharacter &&
!icon.dataset.originalVideoFile
) {
icon.dataset.originalVideoFile =
baseVideoFile || baseVideoURL || "";
}
// Desabilita/habilita skills baseado no personagem ativo
if (onlyCharacter) {
// Skill específica de um personagem
if (onlyCharacter === activeCharacter) {
// Skill do personagem ativo → habilitar
icon.style.opacity = "1";
icon.style.filter = "";
icon.style.pointerEvents = "";
icon.classList.remove("disabled-skill");
} else {
// Skill de outro personagem → desabilitar (escuras)
icon.style.opacity = "0.3";
icon.style.filter = "grayscale(100%)";
icon.style.pointerEvents = "none";
icon.classList.add("disabled-skill");
}
} else {
// Skill compartilhada → sempre habilitada
icon.style.opacity = "1";
icon.style.filter = "";
icon.style.pointerEvents = "";
icon.classList.remove("disabled-skill");
// Atualiza vídeo se houver character_videos e personagem ativo
if (hasCharacterVideos && activeCharacter) {
try {
const characterVideos = JSON.parse(
icon.dataset.characterVideos
);
const characterVideo = characterVideos[activeCharacter];
if (characterVideo && characterVideo.trim() !== "") {
icon.dataset.videoFile = characterVideo;
icon.dataset.video =
filePathURL(characterVideo) || characterVideo;
}
} catch (e) {
console.error("[Swap] Erro ao processar character_videos:", e);
}
} else if (hasCharacterVideos && activeCharacter === null) {
// Restaura vídeo original quando volta ao personagem padrão (null)
const originalVideo = icon.dataset.originalVideoFile || "";
if (originalVideo) {
icon.dataset.videoFile = originalVideo;
icon.dataset.video =
filePathURL(originalVideo) || originalVideo;
}
}
}
}
);
}
// Troca entre personagens (genérico)
function handleSwapCharacter(swapIconEl) {
if (!swapIconEl) return;
const iconBar = document.querySelector(".icon-bar");
if (!iconBar) return;
const availableCharacters = detectAvailableCharacters();
if (availableCharacters.length === 0) {
// Se não há personagens específicos, não faz nada (não há swap)
return;
}
// Se activeCharacter é null, inicializa com o primeiro personagem disponível
if (activeCharacter === null) {
activeCharacter = availableCharacters[0];
} else {
// Encontra o índice do personagem atual e avança para o próximo
const currentIndex = availableCharacters.indexOf(activeCharacter);
const nextIndex = (currentIndex + 1) % availableCharacters.length;
activeCharacter = availableCharacters[nextIndex];
}
// Aplica o novo estado
applyCharacterSwapState();
// Atualiza todas as skills na barra
Array.from(iconBar.querySelectorAll(".skill-icon[data-index]")).forEach(
(icon) => {
const onlyCharacter = (icon.dataset.onlyCharacter || "").trim();
const hasCharacterVideos =
icon.dataset.characterVideos &&
icon.dataset.characterVideos.trim() !== "";
const baseVideoFile = icon.dataset.videoFile || "";
const baseVideoURL = icon.dataset.video || "";
// Salva o vídeo original se ainda não foi salvo
if (hasCharacterVideos && !icon.dataset.originalVideoFile) {
icon.dataset.originalVideoFile =
baseVideoFile || baseVideoURL || "";
}
// Desabilita/habilita skills baseado no personagem ativo
if (onlyCharacter) {
// Skill específica de um personagem
if (onlyCharacter === activeCharacter) {
// Skill do personagem ativo → habilitar
icon.style.opacity = "1";
icon.style.filter = "";
icon.style.pointerEvents = "";
icon.classList.remove("disabled-skill");
} else {
// Skill de outro personagem → desabilitar (escuras)
icon.style.opacity = "0.3";
icon.style.filter = "grayscale(100%)";
icon.style.pointerEvents = "none";
icon.classList.add("disabled-skill");
}
} else {
// Skill compartilhada → sempre habilitada
icon.style.opacity = "1";
icon.style.filter = "";
icon.style.pointerEvents = "";
icon.classList.remove("disabled-skill");
// Atualiza vídeo se houver character_videos e personagem ativo
if (hasCharacterVideos && activeCharacter) {
try {
const characterVideos = JSON.parse(
icon.dataset.characterVideos
);
const characterVideo = characterVideos[activeCharacter];
if (characterVideo && characterVideo.trim() !== "") {
icon.dataset.videoFile = characterVideo;
icon.dataset.video =
filePathURL(characterVideo) || characterVideo;
}
} catch (e) {
console.error("[Swap] Erro ao processar character_videos:", e);
}
} else if (hasCharacterVideos && activeCharacter === null) {
// Restaura vídeo original quando volta ao personagem padrão (null)
const originalVideo = icon.dataset.originalVideoFile || "";
if (originalVideo) {
icon.dataset.videoFile = originalVideo;
icon.dataset.video =
filePathURL(originalVideo) || originalVideo;
}
}
}
}
);
}
// Detecta qual forma está atualmente visível no DOM
function detectCurrentForm() {
const iconBar = document.querySelector(".icon-bar");
if (!iconBar || !formsData || Object.keys(formsData).length === 0)
return null;
// Coleta todas as skills de form que estão visíveis no DOM
const allFormSkillNames = new Set();
Object.values(formsData).forEach((form) => {
if (form.order && Array.isArray(form.order)) {
form.order.forEach((skillName) => allFormSkillNames.add(skillName));
}
});
const visibleFormSkillNames = new Set();
Array.from(iconBar.querySelectorAll(".skill-icon[data-index]")).forEach(
(icon) => {
const name = (icon.dataset.nome || "").trim();
if (name && allFormSkillNames.has(name)) {
visibleFormSkillNames.add(name);
}
}
);
// Compara com cada forma para ver qual corresponde
for (const [formName, formData] of Object.entries(formsData)) {
if (formData.order && Array.isArray(formData.order)) {
const formSkillSet = new Set(formData.order);
// Verifica se todas as skills desta forma estão visíveis
let allMatch = true;
for (const skillName of formData.order) {
if (!visibleFormSkillNames.has(skillName)) {
allMatch = false;
break;
}
}
if (
allMatch &&
formData.order.length === visibleFormSkillNames.size
) {
return formName;
}
}
}
return null; }
function switchForm() {
const skillsRoot = document.getElementById("skills");
if (!skillsRoot) return;
// Lê dados de forms do atributo data-forms
try {
const formsJSON = skillsRoot.dataset.forms || "{}";
if (formsJSON && formsJSON !== "{}") {
formsData = JSON.parse(formsJSON);
}
} catch (e) {
console.error("[Forms] Erro ao parsear forms:", e);
return;
}
if (!formsData || Object.keys(formsData).length === 0) {
return; // Não tem forms, não faz nada
}
// Identifica skills fixas (sempre presentes)
const iconBar = document.querySelector(".icon-bar");
if (!iconBar) return;
// Busca a skill com form_switch dinamicamente (genérico)
const changeFormIcon = Array.from(
iconBar.querySelectorAll(".skill-icon[data-index]")
).find(
(icon) =>
icon.dataset.formSwitch === "true" ||
icon.getAttribute("data-form-switch") === "true"
);
if (changeFormIcon) {
changeFormIcon.classList.add("active");
}
// Determina skills fixas dinamicamente: todas que não estão em nenhuma forms
const allFormSkillNames = new Set();
Object.values(formsData).forEach((form) => {
if (form.order && Array.isArray(form.order)) {
form.order.forEach((skillName) => allFormSkillNames.add(skillName));
}
});
// Skills fixas = todas as skills na barra que não estão em nenhuma forms
fixedSkills = Array.from(
iconBar.querySelectorAll(".skill-icon[data-index]")
)
.filter((icon) => {
const name = (icon.dataset.nome || "").trim();
return name && !allFormSkillNames.has(name);
})
.map((icon) => snapshotIconData(icon));
// Obtém lista de formas disponíveis
const formNames = Object.keys(formsData);
if (formNames.length === 0) return;
// Determina próxima forma
// Se currentForm é null, detecta qual forma está atualmente visível no DOM
if (currentForm === null) {
currentForm = detectCurrentForm();
}
// Se ainda não conseguiu detectar, usa a primeira forma como fallback
if (!currentForm && formNames.length > 0) {
currentForm = formNames[0];
}
// Cria ordem circular fixa baseada na forma atual detectada
// Se detectamos "Brain Point", ordem é: Brain → Kung Fu → Heavy → Brain
// Se detectamos "Kung Fu Point", ordem é: Kung Fu → Heavy → Brain → Kung Fu
// Se detectamos "Heavy Point", ordem é: Heavy → Brain → Kung Fu → Heavy
let orderedFormNames = [];
if (currentForm === "Brain Point" && formNames.length === 3) {
// Ordem conhecida: Brain → Kung Fu → Heavy
if (
formNames.includes("Kung Fu Point") &&
formNames.includes("Heavy Point")
) {
orderedFormNames = ["Brain Point", "Kung Fu Point", "Heavy Point"];
}
} else if (currentForm === "Kung Fu Point" && formNames.length === 3) {
// Ordem conhecida: Kung Fu → Heavy → Brain
if (
formNames.includes("Heavy Point") &&
formNames.includes("Brain Point")
) {
orderedFormNames = ["Kung Fu Point", "Heavy Point", "Brain Point"];
}
} else if (currentForm === "Heavy Point" && formNames.length === 3) {
// Ordem conhecida: Heavy → Brain → Kung Fu
if (
formNames.includes("Brain Point") &&
formNames.includes("Kung Fu Point")
) {
orderedFormNames = ["Heavy Point", "Brain Point", "Kung Fu Point"];
}
}
// Se não conseguiu criar ordem conhecida, usa ordem alfabética como fallback
if (orderedFormNames.length === 0) {
orderedFormNames = [...formNames].sort();
// Se sabemos a forma atual, reorganiza para começar por ela
if (currentForm) {
const currentIdx = orderedFormNames.indexOf(currentForm);
if (currentIdx !== -1) {
orderedFormNames = [
...orderedFormNames.slice(currentIdx),
...orderedFormNames.slice(0, currentIdx),
];
}
}
}
const currentIdx = orderedFormNames.indexOf(currentForm);
if (currentIdx === -1) return; // Forma não encontrada
const nextIdx = (currentIdx + 1) % orderedFormNames.length;
const nextForm = orderedFormNames[nextIdx];
currentForm = nextForm;
// Atualiza barra de skills (que vai remover o active depois da animação)
updateSkillsBarForForm(nextForm, formsData[nextForm], changeFormIcon);
}
function snapshotIconData(icon) {
const img = icon.querySelector("img");
const iconURL = img ? img.src : "";
const subsRaw = icon.dataset.subs || icon.getAttribute("data-subs") || "";
let subs = null;
try {
subs = subsRaw ? JSON.parse(subsRaw) : null;
} catch {
subs = null;
}
let flags = null;
if (icon.dataset.flags) {
try {
flags = JSON.parse(icon.dataset.flags);
} catch (e) { }
}
let weapon = null;
if (icon.dataset.weapon) {
try {
weapon = JSON.parse(icon.dataset.weapon);
} catch (e) { }
}
return {
name: icon.dataset.nome || icon.dataset.name || "",
index: icon.dataset.index || "",
level: icon.dataset.level || "",
desc: icon.dataset.desc || "",
descPt: icon.dataset.descPt || "",
descEn: icon.dataset.descEn || "",
descEs: icon.dataset.descEs || "",
descPl: icon.dataset.descPl || "",
attrs: icon.dataset.atr || icon.dataset.attrs || "",
video: icon.dataset.video || "",
iconURL,
iconFile: icon.dataset.iconFile || "",
subs,
flags,
weapon,
formSwitch: icon.dataset.formSwitch || "",
};
}
function updateSkillsBarForForm(formName, formData, changeFormIconEl) {
const iconBar = document.querySelector(".icon-bar");
if (!iconBar || !formData || !formData.skills) return;
// Determina skills fixas dinamicamente: todas que não estão em nenhuma forms
const allFormSkillNames = new Set();
Object.values(formsData).forEach((form) => {
if (form.order && Array.isArray(form.order)) {
form.order.forEach((skillName) => allFormSkillNames.add(skillName));
}
});
// Remove skills de forma antigas (que não são fixas) com animação de saída
const existingIcons = Array.from(
iconBar.querySelectorAll(".skill-icon[data-index]")
);
const iconsToRemove = [];
existingIcons.forEach((icon) => {
const name = (icon.dataset.nome || "").trim();
if (name && allFormSkillNames.has(name)) {
iconsToRemove.push(icon);
}
});
// Anima saída das skills antigas
iconsToRemove.forEach((icon) => {
icon.style.transition = "opacity .15s ease, transform .15s ease";
icon.style.opacity = "0";
icon.style.transform = "translateY(-6px)";
});
// Encontra a skill com form_switch dinamicamente (genérico)
const changeFormIcon = Array.from(
iconBar.querySelectorAll(".skill-icon[data-index]")
).find(
(icon) =>
icon.dataset.formSwitch === "true" ||
icon.getAttribute("data-form-switch") === "true"
);
// Encontra a próxima skill fixa depois do form_switch dinamicamente
let nextFixedSkillIcon = null;
if (changeFormIcon) {
const allIcons = Array.from(
iconBar.querySelectorAll(".skill-icon[data-index]")
).sort((a, b) => {
return (
parseInt(a.dataset.index || "0", 10) -
parseInt(b.dataset.index || "0", 10)
);
});
const changeFormIndex = allIcons.indexOf(changeFormIcon);
const allFormSkillNames = new Set();
Object.values(formsData).forEach((form) => {
if (form.order && Array.isArray(form.order)) {
form.order.forEach((skillName) => allFormSkillNames.add(skillName));
}
});
// Procura a próxima skill fixa (que não está em forms)
for (let i = changeFormIndex + 1; i < allIcons.length; i++) {
const icon = allIcons[i];
const name = (icon.dataset.nome || "").trim();
if (name && !allFormSkillNames.has(name)) {
nextFixedSkillIcon = icon;
break;
}
}
}
if (!changeFormIcon || !nextFixedSkillIcon) {
console.warn(
"[Forms] Skill com form_switch ou próxima skill fixa não encontrada"
);
return;
}
const formSkills = formData.skills || [];
const formOrder = formData.order || [];
// Primeira skill da forma (vai ANTES do Change Form)
const firstSkillName = formOrder[0];
const firstSkillData = formSkills.find((s) => s.name === firstSkillName);
// Terceira skill da forma (vai DEPOIS do Change Form, ANTES do Guard Point)
const thirdSkillName = formOrder[1]; // Segunda na ordem = terceira skill (índice 1)
const thirdSkillData = formSkills.find((s) => s.name === thirdSkillName);
// Quinta skill da forma (vai DEPOIS do Guard Point)
const fifthSkillName = formOrder[2]; // Terceira na ordem = quinta skill (índice 2)
const fifthSkillData = formSkills.find((s) => s.name === fifthSkillName);
// Cria fragments para inserir
const firstFragment = document.createDocumentFragment();
const thirdFragment = document.createDocumentFragment();
const fifthFragment = document.createDocumentFragment();
if (firstSkillData) {
const iconElement = createSkillIconElement(firstSkillData, 1);
firstFragment.appendChild(iconElement);
}
if (thirdSkillData) {
const changeFormIndex = parseInt(
changeFormIcon.dataset.index || "2",
10
);
const iconElement = createSkillIconElement(
thirdSkillData,
changeFormIndex + 1
);
thirdFragment.appendChild(iconElement);
}
if (fifthSkillData) {
const nextFixedSkillIndex = parseInt(
nextFixedSkillIcon.dataset.index || "4",
10
);
const iconElement = createSkillIconElement(
fifthSkillData,
nextFixedSkillIndex + 1
);
fifthFragment.appendChild(iconElement);
}
// Remove os ícones antigos do DOM após animação
setTimeout(() => {
iconsToRemove.forEach((icon) => {
if (icon.parentNode) {
icon.remove();
}
});
// Insere a primeira skill ANTES da skill com form_switch
if (firstFragment.hasChildNodes()) {
iconBar.insertBefore(firstFragment, changeFormIcon);
}
// Insere a terceira skill DEPOIS da skill com form_switch, ANTES da próxima skill fixa
if (thirdFragment.hasChildNodes()) {
iconBar.insertBefore(thirdFragment, nextFixedSkillIcon);
}
// Insere a quinta skill DEPOIS da próxima skill fixa
if (fifthFragment.hasChildNodes()) {
if (nextFixedSkillIcon.nextSibling) {
iconBar.insertBefore(fifthFragment, nextFixedSkillIcon.nextSibling);
} else {
iconBar.appendChild(fifthFragment);
}
}
// Anima entrada das novas skills (similar a animateIconsBarEntrance)
const newIconsInBar = Array.from(
iconBar.querySelectorAll(".skill-icon[data-index]")
).filter((icon) => {
const name = (icon.dataset.nome || "").trim();
return name && allFormSkillNames.has(name);
});
newIconsInBar.forEach((icon, idx) => {
icon.style.opacity = "0";
icon.style.transform = "translateY(6px)";
requestAnimationFrame(() => {
setTimeout(() => {
icon.style.transition = "opacity .18s ease, transform .18s ease";
icon.style.opacity = "1";
icon.style.transform = "translateY(0)";
}, idx * 24);
});
});
// Remove slots de descrição das skills de forma antigas e cria novos
const descBox = document.querySelector(".skills-details .desc-box");
if (descBox) {
// Encontra os slots de descrição dos elementos fixos
const changeFormIndex = parseInt(
changeFormIcon.dataset.index || "1",
10
);
const nextFixedSkillIndex = parseInt(
nextFixedSkillIcon.dataset.index || "1",
10
);
const changeFormDescSlot = descBox.querySelector(
`.skill-desc[data-index="${changeFormIndex}"]`
);
const nextFixedSkillDescSlot = descBox.querySelector(
`.skill-desc[data-index="${nextFixedSkillIndex}"]`
);
// Cria slot para primeira skill (antes do Change Form)
if (firstSkillData && changeFormDescSlot) {
const descSlot = document.createElement("div");
descSlot.className = "skill-desc";
descSlot.setAttribute("data-index", changeFormIndex);
descBox.insertBefore(descSlot, changeFormDescSlot);
}
// Cria slot para terceira skill (depois da skill com form_switch, antes da próxima skill fixa)
if (thirdSkillData && nextFixedSkillDescSlot) {
const descSlot = document.createElement("div");
descSlot.className = "skill-desc";
descSlot.setAttribute("data-index", nextFixedSkillIndex);
descBox.insertBefore(descSlot, nextFixedSkillDescSlot);
}
// Cria slot para quinta skill (depois da próxima skill fixa)
if (fifthSkillData && nextFixedSkillDescSlot) {
const descSlot = document.createElement("div");
descSlot.className = "skill-desc";
descSlot.setAttribute("data-index", nextFixedSkillIndex + 1);
if (nextFixedSkillDescSlot.nextSibling) {
descBox.insertBefore(
descSlot,
nextFixedSkillDescSlot.nextSibling
);
} else {
descBox.appendChild(descSlot);
}
}
}
// Re-numera todas as skills na ordem do DOM
const allIcons = Array.from(
iconBar.querySelectorAll(".skill-icon[data-index]")
);
let currentIndex = 1;
allIcons.forEach((icon) => {
const oldIndex = icon.dataset.index;
icon.setAttribute("data-index", currentIndex);
// Atualiza slot de descrição
if (descBox && oldIndex) {
const descSlot = descBox.querySelector(
`.skill-desc[data-index="${oldIndex}"]`
);
if (descSlot) {
descSlot.setAttribute("data-index", currentIndex);
}
}
currentIndex++;
});
// Re-wire eventos
wireClicksForCurrentBar();
wireTooltipsForNewIcons();
// Remove active do Change Form após animação completar
setTimeout(() => {
if (changeFormIconEl) {
changeFormIconEl.classList.remove("active");
}
}, newIconsInBar.length * 24 + 180);
}, 150);
}
function createSkillIconElement(skill, index) {
const iconWrap = document.createElement("div");
iconWrap.className = "skill-icon";
iconWrap.setAttribute("data-index", index);
iconWrap.setAttribute("data-nome", skill.name || "");
iconWrap.setAttribute("data-desc", "");
iconWrap.setAttribute(
"data-atr",
makeAttrString(
skill.powerpve,
skill.powerpvp,
skill.energy,
skill.cooldown
)
);
iconWrap.setAttribute(
"data-video",
skill.video ? filePathURL(skill.video) : ""
);
iconWrap.setAttribute("data-video-preload", "auto");
iconWrap.setAttribute("data-icon-file", skill.icon || "");
iconWrap.setAttribute("data-video-file", skill.video || "");
if (
skill.level &&
skill.level !== "" &&
skill.level.toUpperCase() !== "NIVEL"
) {
iconWrap.setAttribute("data-level", skill.level);
}
if (skill.desc_i18n) {
if (skill.desc_i18n.pt)
iconWrap.setAttribute("data-desc-pt", skill.desc_i18n.pt);
if (skill.desc_i18n.en)
iconWrap.setAttribute("data-desc-en", skill.desc_i18n.en);
if (skill.desc_i18n.es)
iconWrap.setAttribute("data-desc-es", skill.desc_i18n.es);
if (skill.desc_i18n.pl)
iconWrap.setAttribute("data-desc-pl", skill.desc_i18n.pl);
}
if (skill.subs && Array.isArray(skill.subs) && skill.subs.length > 0) {
iconWrap.setAttribute("data-subs", JSON.stringify(skill.subs));
}
if (
skill.suborder &&
Array.isArray(skill.suborder) &&
skill.suborder.length > 0
) {
iconWrap.setAttribute("data-suborder", JSON.stringify(skill.suborder));
}
if (skill.flags && Array.isArray(skill.flags) && skill.flags.length > 0) {
iconWrap.setAttribute("data-flags", JSON.stringify(skill.flags));
}
if (
skill.weapon &&
typeof skill.weapon === "object" &&
Object.keys(skill.weapon).length > 0
) {
iconWrap.setAttribute("data-weapon", JSON.stringify(skill.weapon));
}
if (skill.effect && typeof skill.effect === "object") {
try {
iconWrap.setAttribute("data-effect", JSON.stringify(skill.effect));
} catch (e) { }
}
const img = document.createElement("img");
img.className = "skill-icon-img";
img.src = filePathURL(skill.icon || "");
img.alt = "";
iconWrap.appendChild(img);
return iconWrap; }
function createSkillIcon(skill, index, container) {
// Cria ícone similar ao que é gerado pelo servidor
const iconWrap = document.createElement("div");
iconWrap.className = "skill-icon";
iconWrap.setAttribute("data-index", index);
iconWrap.setAttribute("data-nome", skill.name || "");
iconWrap.setAttribute("data-desc", "");
iconWrap.setAttribute(
"data-atr",
makeAttrString(
skill.powerpve,
skill.powerpvp,
skill.energy,
skill.cooldown
)
);
iconWrap.setAttribute(
"data-video",
skill.video ? filePathURL(skill.video) : ""
);
iconWrap.setAttribute("data-video-preload", "auto");
iconWrap.setAttribute("data-icon-file", skill.icon || "");
iconWrap.setAttribute("data-video-file", skill.video || "");
if (
skill.level &&
skill.level !== "" &&
skill.level.toUpperCase() !== "NIVEL"
) {
iconWrap.setAttribute("data-level", skill.level);
}
if (skill.desc_i18n) {
if (skill.desc_i18n.pt)
iconWrap.setAttribute("data-desc-pt", skill.desc_i18n.pt);
if (skill.desc_i18n.en)
iconWrap.setAttribute("data-desc-en", skill.desc_i18n.en);
if (skill.desc_i18n.es)
iconWrap.setAttribute("data-desc-es", skill.desc_i18n.es);
if (skill.desc_i18n.pl)
iconWrap.setAttribute("data-desc-pl", skill.desc_i18n.pl);
}
if (skill.subs && Array.isArray(skill.subs) && skill.subs.length > 0) {
iconWrap.setAttribute("data-subs", JSON.stringify(skill.subs));
}
if (
skill.suborder &&
Array.isArray(skill.suborder) &&
skill.suborder.length > 0
) {
iconWrap.setAttribute("data-suborder", JSON.stringify(skill.suborder));
}
if (skill.flags && Array.isArray(skill.flags) && skill.flags.length > 0) {
iconWrap.setAttribute("data-flags", JSON.stringify(skill.flags));
}
if (
skill.weapon &&
typeof skill.weapon === "object" &&
Object.keys(skill.weapon).length > 0
) {
iconWrap.setAttribute("data-weapon", JSON.stringify(skill.weapon));
}
if (skill.effect && typeof skill.effect === "object") {
try {
iconWrap.setAttribute("data-effect", JSON.stringify(skill.effect));
} catch (e) { }
}
const img = document.createElement("img");
img.className = "skill-icon-img";
img.src = filePathURL(skill.icon || "");
img.alt = "";
iconWrap.appendChild(img);
container.appendChild(iconWrap); }
function makeAttrString(pve, pvp, energy, cd) {
const parts = [
pve !== undefined && pve !== null && pve !== "" ? String(pve) : "-",
pvp !== undefined && pvp !== null && pvp !== "" ? String(pvp) : "-",
energy !== undefined && energy !== null && energy !== ""
? String(energy)
: "-",
cd !== undefined && cd !== null && cd !== "" ? String(cd) : "-",
];
return parts.join(", ");
}
function filePathURL(fileName) {
// Evita requisições para valores vazios
if (!fileName || fileName.trim() === "") {
return "";
}
const f = encodeURIComponent(fileName.replace(/^Arquivo:|^File:/, ""));
const base =
window.mw && mw.util && typeof mw.util.wikiScript === "function"
? mw.util.wikiScript()
: window.mw && window.mw.config
? mw.config.get("wgScript") || "/index.php"
: "/index.php";
// Garante HTTPS para evitar Mixed Content
let url = `${base}?title=Especial:FilePath/${f}`;
if (window.location.protocol === "https:" && url.startsWith("http://")) {
url = url.replace("http://", "https://");
}
return url;
}
function slugify(s) {
if (!s) return "";
return String(s)
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^\w\s-]/g, "")
.replace(/[\s:/\-]+/g, "-")
.replace(/^-+|-+$/g, "")
.replace(/-+/g, "-");
}
window.__skillSlugify = slugify;
function getLangKey() {
const skillsRoot = document.getElementById("skills");
const raw = (
document.documentElement.lang ||
skillsRoot?.dataset.i18nDefault ||
"pt"
).toLowerCase();
return raw === "pt-br" ? "pt" : raw.split("-")[0] || "pt";
}
function chooseDescFrom(obj) {
const lang = getLangKey();
// Aceita tanto desc_i18n quanto desc para compatibilidade
const pack = obj.desc_i18n ||
obj.desc || {
pt: obj.descPt,
en: obj.descEn,
es: obj.descEs,
pl: obj.descPl,
};
return (
(pack && (pack[lang] || pack.pt || pack.en || pack.es || pack.pl)) || ""
);
}
function renderSubAttributesFromObj(s, L) {
const chip = (label, val) =>
val
? `
${label}${val}
`
: "";
const pve = (s.powerpve || "").toString().trim();
const pvp = (s.powerpvp || "").toString().trim();
const en = (s.energy || "").toString().trim();
const cd = (s.cooldown || "").toString().trim();
const rows = [
cd ? chip(L.cooldown, cd) : "",
en
? chip(
en.startsWith("-") ? L.energy_cost : L.energy_gain,
en.startsWith("-") ? en.replace(/^-/, "") : en.replace(/^\+?/, "")
)
: "",
pve ? chip(L.power, pve) : "",
pvp ? chip(L.power_pvp, pvp) : "",
].filter(Boolean);
return rows.length ? `
${rows.join("")}
` : "";
}
function getFlagIconURL(key) {
if (!FLAG_ICON_FILES[key]) return "";
if (!flagIconURLCache.has(key)) {
flagIconURLCache.set(key, filePathURL(FLAG_ICON_FILES[key]));
}
return flagIconURLCache.get(key);
}
function renderFlagsRow(flags) {
const arr = (flags || []).filter(Boolean);
if (!arr.length) return "";
const cacheKey = arr.join("|");
if (flagRowCache.has(cacheKey)) {
return flagRowCache.get(cacheKey);
}
const items = arr
.map((k) => {
const url = getFlagIconURL(k);
return url
? `<img class="skill-flag" data-flag="${k}" alt="" src="${url}">`
: "";
})
.join("");
const html = items
? `
${items}
`
: "";
if (html) flagRowCache.set(cacheKey, html);
return html;
}
function applyFlagTooltips(container) {
const skillsRoot = document.getElementById("skills");
if (!skillsRoot) return;
let pack = {};
try {
pack = JSON.parse(skillsRoot.dataset.i18nFlags || "{}");
} catch (e) { }
const lang = getLangKey();
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();
});
});
}
// ====== Skill/Subskill inheritance helpers ======
const mainSkillsMeta = {
byIndex: new Map(),
byName: new Map(),
ready: false,
};
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 extractFileNameFromURL(url) {
if (!url) return "";
const match = String(url).match(/(?:FilePath\/)([^&?]+)/i);
return match ? decodeURIComponent(match[1]) : "";
}
function parseAttrString(raw) {
const parts = (raw || "").split(",").map((v) => v.trim());
const safe = (idx) => {
const val = parts[idx] || "";
return val && val !== "-" ? val : "";
};
return {
powerpve: safe(0),
powerpvp: safe(1),
energy: safe(2),
cooldown: safe(3),
};
}
function hasText(value) {
return typeof value === "string"
? value.trim() !== ""
: value !== undefined && value !== null;
}
function pickFilled(current, fallback) {
if (current === 0 || current === "0") return current;
if (!hasText(current)) return fallback;
return current;
}
function buildMainSkillsMeta(nodes) {
if (mainSkillsMeta.ready) {
return mainSkillsMeta;
}
(nodes || []).forEach((icon) => {
const index = (icon.dataset.index || "").trim();
if (!index) return;
const name = (icon.dataset.nome || icon.dataset.name || "").trim();
const attrs = parseAttrString(icon.dataset.atr || "");
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]) : "";
}
let videoFile = (icon.dataset.videoFile || "").trim();
if (!videoFile) {
videoFile = extractFileNameFromURL(icon.dataset.video || "");
}
const meta = {
index,
name,
icon: iconFile || "",
level: icon.dataset.level || "",
video: videoFile || "",
powerpve: attrs.powerpve || "",
powerpvp: attrs.powerpvp || "",
energy: attrs.energy || "",
cooldown: attrs.cooldown || "",
desc: icon.dataset.desc || "",
descPt: icon.dataset.descPt || "",
descEn: icon.dataset.descEn || "",
descEs: icon.dataset.descEs || "",
descPl: icon.dataset.descPl || "",
};
mainSkillsMeta.byIndex.set(index, meta);
mainSkillsMeta.byIndex.set(parseInt(index, 10), meta);
if (name) {
mainSkillsMeta.byName.set(name, meta);
}
});
mainSkillsMeta.ready = true;
return mainSkillsMeta;
}
function inheritSubskillFromMain(sub, meta) {
if (!sub || !meta) return sub;
// Verifica se herança está desabilitada
const shouldInherit = !(
sub.inherit === false ||
sub.inherit === "no" ||
sub.inherit === "false"
);
// Suporta refS (novo) e refM (legado)
const refS = ((sub.refS || sub.S || sub.s || "") + "").trim();
const refIndex = ((sub.refM || sub.M || sub.m || "") + "").trim();
let name = (sub.name || sub.n || "").trim();
let main = null;
// Se herança está desabilitada, não busca a skill principal
if (!shouldInherit) {
return { ...sub };
}
// Primeiro tenta por refS
if (refS) {
main = meta.byIndex.get(refS) || meta.byIndex.get(parseInt(refS, 10));
}
// Depois por refM
if (!main && refIndex) {
main =
meta.byIndex.get(refIndex) ||
meta.byIndex.get(parseInt(refIndex, 10));
}
// Por último pelo nome
if (!main && name) {
main = meta.byName.get(name);
}
if (!main) {
return sub;
}
const hydrated = { ...sub };
if (!name && main.name) {
name = main.name;
}
hydrated.name = name || hydrated.name || main.name || "";
hydrated.icon = pickFilled(hydrated.icon, main.icon || "");
hydrated.level = pickFilled(hydrated.level, main.level || "");
// Vídeo NUNCA é herdado da skill principal
const hasOwnVideo = Object.prototype.hasOwnProperty.call(sub, "video");
hydrated.video = hasOwnVideo ? sub.video || "" : "";
hydrated.powerpve = pickFilled(hydrated.powerpve, main.powerpve || "");
hydrated.powerpvp = pickFilled(hydrated.powerpvp, main.powerpvp || "");
hydrated.energy = pickFilled(hydrated.energy, main.energy || "");
hydrated.cooldown = pickFilled(hydrated.cooldown, main.cooldown || "");
// Descrição: sempre vem da subskill, nunca herda
// PROTEÇÃO TOTAL: Remove qualquer descrição que possa ter sido copiada do main
if (
!sub.desc &&
!sub.descPt &&
!sub.descEn &&
!sub.descEs &&
!sub.descPl &&
!sub.desc_i18n
) {
hydrated.desc = undefined;
hydrated.descPt = undefined;
hydrated.descEn = undefined;
hydrated.descEs = undefined;
hydrated.descPl = undefined;
hydrated.desc_i18n = undefined;
} else {
// Se subskill tem descrição, normaliza para desc_i18n
if (
!hydrated.desc_i18n &&
(hydrated.descPt ||
hydrated.descEn ||
hydrated.descEs ||
hydrated.descPl)
) {
hydrated.desc_i18n = {
pt: hydrated.descPt || "",
en: hydrated.descEn || "",
es: hydrated.descEs || "",
pl: hydrated.descPl || "",
};
}
}
return hydrated; }
function inheritSubskillTree(subs, meta) {
if (!Array.isArray(subs)) return [];
return subs.map((sub) => {
const hydrated = inheritSubskillFromMain(sub, meta);
if (Array.isArray(hydrated.subs)) {
hydrated.subs = inheritSubskillTree(hydrated.subs, meta);
}
return hydrated;
});
}
function collectAssetsFromSubs(subs, iconsSet, videosSet, flagsSet) {
if (!Array.isArray(subs)) return;
subs.forEach((sub) => {
const iconURL = normalizeFileURL(sub.icon || "", "");
if (iconURL && iconURL !== "") iconsSet.add(iconURL);
// Vídeo normal
if (sub.video && sub.video.trim() !== "") {
const videoURL = normalizeFileURL(sub.video);
if (videoURL) videosSet.add(videoURL);
}
// Vídeo de weapon
if (
sub.weapon &&
typeof sub.weapon === "object" &&
sub.weapon.video &&
sub.weapon.video.trim() !== ""
) {
const weaponVideoURL = normalizeFileURL(sub.weapon.video);
if (weaponVideoURL) videosSet.add(weaponVideoURL);
}
if (Array.isArray(sub.flags)) {
sub.flags.forEach((flagKey) => {
const url = getFlagIconURL(flagKey);
if (url) flagsSet.add(url);
});
}
if (Array.isArray(sub.subs)) {
collectAssetsFromSubs(sub.subs, iconsSet, videosSet, flagsSet);
}
});
}
function buildAssetManifest() {
if (window.__skillAssetManifest && window.__skillAssetManifest.ready) {
return window.__skillAssetManifest;
}
const iconsSet = new Set();
const videosSet = new Set();
const flagsSet = new Set();
iconItems.forEach((el) => {
const img = el.querySelector("img");
if (img && img.src) {
iconsSet.add(img.src);
} else if (el.dataset.iconFile) {
const iconURL = normalizeFileURL(el.dataset.iconFile);
if (iconURL) iconsSet.add(iconURL);
}
// Vídeo normal da skill
const videoRaw = (
el.dataset.videoFile ||
el.dataset.video ||
""
).trim();
if (videoRaw) {
const videoURL = normalizeFileURL(videoRaw);
if (videoURL) videosSet.add(videoURL);
}
// Vídeo de weapon da skill
if (el.dataset.weapon) {
try {
const weaponData = JSON.parse(el.dataset.weapon);
if (
weaponData &&
weaponData.video &&
weaponData.video.trim() !== ""
) {
const weaponVideoURL = normalizeFileURL(weaponData.video);
if (weaponVideoURL) videosSet.add(weaponVideoURL);
}
} catch (e) { }
}
if (el.dataset.flags) {
try {
const parsedFlags = JSON.parse(el.dataset.flags);
(parsedFlags || []).forEach((flagKey) => {
const url = getFlagIconURL(flagKey);
if (url) flagsSet.add(url);
});
} catch (e) { }
}
if (el.dataset.subs) {
try {
const subs = JSON.parse(el.dataset.subs);
collectAssetsFromSubs(subs, iconsSet, videosSet, flagsSet);
} catch (e) { }
}
});
Object.keys(FLAG_ICON_FILES).forEach((flagKey) => {
const url = getFlagIconURL(flagKey);
if (url) flagsSet.add(url);
});
const manifest = {
icons: iconsSet,
videos: videosSet,
flags: flagsSet,
ready: true,
};
window.__skillAssetManifest = manifest;
return manifest;
}
const subskillVideosCache = new Map();
window.__subskillVideosCache = subskillVideosCache;
// Cache de vídeos que falharam (404) para evitar tentativas repetidas const failedVideosCache = new Set(); const missingVideosReported = new Set(); // Para avisar apenas uma vez sobre vídeos faltantes
// Cache de parsing JSON para evitar re-parsear os mesmos dados
const jsonParseCache = new WeakMap();
function getCachedJSON(el, key) {
if (!el) return null;
const cache = jsonParseCache.get(el) || {};
if (cache[key] !== undefined) return cache[key];
const raw = el.dataset[key] || el.getAttribute(`data-${key}`);
if (!raw) {
cache[key] = null;
jsonParseCache.set(el, cache);
return null;
}
try {
const parsed = JSON.parse(raw);
cache[key] = parsed;
jsonParseCache.set(el, cache);
return parsed;
} catch (e) {
cache[key] = null;
jsonParseCache.set(el, cache);
return null;
}
}
let assetManifest = null;
const skillsTab = $("#skills");
const skinsTab = $("#skins");
ensureRemoved(".top-rail");
// NÃO remove .content-card aqui - será gerenciado abaixo
// ensureRemoved('.content-card'); // REMOVIDO - pode estar removendo skills-container
ensureRemoved(".video-placeholder");
Array.from(
document.querySelectorAll(
".card-skins-title, .card-skins .card-skins-title, .cardskins-title, .rail-title"
)
).forEach((t) => {
if ((t.textContent || "").trim().toLowerCase().includes("skins")) {
t.remove();
}
});
if (skillsTab) {
const iconBar = skillsTab.querySelector(".icon-bar");
if (iconBar) {
const rail = document.createElement("div");
rail.className = "top-rail skills";
// Criar wrapper de scroll para permitir glow sem clipping
if (
!iconBar.parentElement ||
!iconBar.parentElement.classList.contains("icon-scroll-x")
) {
const scrollWrapper = document.createElement("div");
scrollWrapper.className = "icon-scroll-x";
scrollWrapper.appendChild(iconBar);
rail.appendChild(scrollWrapper);
} else {
rail.appendChild(iconBar.parentElement);
}
skillsTab.prepend(rail);
}
// Busca skills-container criado pelo Lua
const skillsContainer = skillsTab.querySelector(".skills-container");
// Busca skills-details e video-container (podem estar dentro de skills-container ou soltos)
let details = skillsTab.querySelector(".skills-details");
if (!details && skillsContainer) {
details = skillsContainer.querySelector(".skills-details");
}
if (!details) {
details = document.createElement("div");
details.className = "skills-details";
}
// Busca ou cria desc-box dentro de details
let descBoxEl = details.querySelector(".desc-box");
if (!descBoxEl) {
descBoxEl = document.createElement("div");
descBoxEl.className = "desc-box";
details.appendChild(descBoxEl);
}
// Busca video-container
let videoContainer = skillsTab.querySelector(".video-container");
if (!videoContainer && skillsContainer) {
videoContainer = skillsContainer.querySelector(".video-container");
}
if (!videoContainer) {
videoContainer = document.createElement("div");
videoContainer.className = "video-container";
}
// Cria ou atualiza content-card skills-grid (estrutura esperada pelo CSS)
let card = skillsTab.querySelector(".content-card.skills-grid");
if (!card) {
card = document.createElement("div");
card.className = "content-card skills-grid";
skillsTab.appendChild(card);
}
// Move details e videoContainer para dentro do card (estrutura correta)
// Remove skills-container se existir (não é necessário para o layout)
if (skillsContainer && skillsContainer.parentNode) {
// Move os elementos filhos de skills-container para o card
if (details.parentNode === skillsContainer) {
skillsContainer.removeChild(details);
}
if (videoContainer.parentNode === skillsContainer) {
skillsContainer.removeChild(videoContainer);
}
// Remove skills-container se estiver vazio
if (skillsContainer.children.length === 0) {
skillsContainer.remove();
}
}
// Garante que details e videoContainer estão no card na ordem correta
// skills-details deve vir ANTES de video-container para o grid funcionar
if (details.parentNode !== card) {
// Se videoContainer já está no card, insere details antes dele
if (videoContainer.parentNode === card) {
card.insertBefore(details, videoContainer);
} else {
card.appendChild(details);
}
}
if (videoContainer.parentNode !== card) {
card.appendChild(videoContainer);
}
// Garante ordem: details primeiro, videoContainer depois
if (details.parentNode === card && videoContainer.parentNode === card) {
if (details.nextSibling !== videoContainer) {
card.insertBefore(details, videoContainer);
}
}
}
// Função para obter iconsBar de forma segura (com retry)
function getIconsBar() {
const skillsEl = document.getElementById("skills");
if (!skillsEl) return null;
return skillsEl.querySelector(".icon-bar");
}
const iconsBar = getIconsBar();
const skillsTopRail = iconsBar
? iconsBar.closest(".top-rail.skills")
: null;
const iconItems = iconsBar
? Array.from(iconsBar.querySelectorAll(".skill-icon"))
: [];
buildMainSkillsMeta(iconItems);
// Verifica se há weapon em skills principais OU em subskills
function checkHasAnyWeapon() {
// Verifica skills principais
const hasMainWeapon = iconItems.some((el) => {
const weapon = el.dataset.weapon;
return weapon && weapon.trim() !== "" && weapon !== "{}";
});
if (hasMainWeapon) {
return true;
}
// Verifica subskills
for (const el of iconItems) {
const subsRaw = el.getAttribute("data-subs");
if (!subsRaw) continue;
try {
const subs = JSON.parse(subsRaw);
if (
Array.isArray(subs) &&
subs.some(
(s) =>
s &&
s.weapon &&
typeof s.weapon === "object" &&
Object.keys(s.weapon).length > 0
)
) {
return true;
}
} catch (e) { }
}
return false;
}
const hasWeaponSkillAvailable = checkHasAnyWeapon();
let weaponToggleBtn = null;
if (!assetManifest) {
assetManifest = buildAssetManifest();
// Pré-carrega apenas ícones e flags críticos
if (assetManifest.icons && assetManifest.icons.size) {
assetManifest.icons.forEach((url) => {
if (!url || imagePreloadCache.has(url)) return;
const img = new Image();
img.decoding = "async";
img.loading = "eager";
img.referrerPolicy = "same-origin";
img.src = url;
imagePreloadCache.set(url, img);
});
}
if (assetManifest.flags && assetManifest.flags.size) {
assetManifest.flags.forEach((url) => {
if (!url || imagePreloadCache.has(url)) return;
const img = new Image();
img.decoding = "async";
img.loading = "eager";
img.referrerPolicy = "same-origin";
img.src = url;
imagePreloadCache.set(url, img);
});
}
}
// Busca descBox e videoBox após a estrutura estar organizada
const descBox = $("#skills") ? $(".desc-box", $("#skills")) : null;
const videoBox = $("#skills") ? $(".video-container", $("#skills")) : null;
const videosCache = new Map();
const nestedVideoElByIcon = new WeakMap();
const barStack = [];
window.__barStack = barStack;
let initialBarSnapshot = null;
let totalVideos = 0,
loadedVideos = 0,
autoplay = false;
window.__lastActiveSkillIcon = null;
let userHasInteracted = false;
let globalWeaponEnabled = false;
try {
if (localStorage.getItem("glaWeaponEnabled") === "1") {
globalWeaponEnabled = true;
}
} catch (err) { }
const weaponStateListeners = new Set();
let showWeaponPopupFn = null;
let popupShouldOpen = false;
function attachWeaponPopupFn(fn) {
if (typeof fn !== "function") return;
showWeaponPopupFn = fn;
if (popupShouldOpen) {
popupShouldOpen = false;
try {
showWeaponPopupFn();
} catch (err) { }
}
}
attachWeaponPopupFn(window.__glaWeaponShowPopup);
function requestWeaponPopupDisplay() {
try {
if (localStorage.getItem("glaWeaponPopupDismissed") === "1") return;
} catch (err) { }
if (typeof showWeaponPopupFn === "function") {
showWeaponPopupFn();
return;
}
popupShouldOpen = true;
}
function onWeaponStateChange(fn) {
if (typeof fn !== "function") return;
weaponStateListeners.add(fn);
}
function syncWeaponButtonState(enabled) {
if (!weaponToggleBtn || !weaponToggleBtn.isConnected) return;
// Usa .weapon-active em vez de .active para não conflitar com skills
weaponToggleBtn.classList.toggle("weapon-active", !!enabled);
weaponToggleBtn.classList.remove("active"); // Garante que .active nunca seja aplicado
weaponToggleBtn.setAttribute("aria-pressed", enabled ? "true" : "false");
weaponToggleBtn.setAttribute(
"aria-label",
enabled ? "Desativar Arma Especial" : "Ativar Arma Especial"
);
}
function syncWeaponRailState(enabled) {
if (skillsTopRail) {
skillsTopRail.classList.toggle("weapon-mode-on", !!enabled);
}
}
function notifyWeaponStateListeners(enabled) {
weaponStateListeners.forEach((listener) => {
try {
listener(enabled);
} catch (err) { }
});
}
let pendingWeaponState = null;
window.addEventListener("weapon:ready", (ev) => {
if (ev && ev.detail && ev.detail.showPopup) {
attachWeaponPopupFn(ev.detail.showPopup);
}
if (pendingWeaponState === null) return;
if (typeof window.__applyWeaponState === "function") {
const target = pendingWeaponState;
pendingWeaponState = null;
window.__applyWeaponState(target);
}
});
window.__setGlobalWeaponEnabled = (enabled) => {
globalWeaponEnabled = enabled;
notifyWeaponStateListeners(enabled);
};
function requestWeaponState(targetState) {
if (typeof window.__applyWeaponState === "function") {
pendingWeaponState = null;
window.__applyWeaponState(targetState);
return;
}
pendingWeaponState = targetState;
}
onWeaponStateChange(syncWeaponButtonState);
function reapplyWeaponClassesToBar() {
// SISTEMA UNIFICADO: Aplica em skills E subskills (sempre, não só quando weapon está ativo)
// Obtém iconsBar dinamicamente (pode não estar pronto ainda quando há flags)
const currentIconsBar = getIconsBar();
if (!currentIconsBar) {
return;
}
// Busca em skills principais (dentro de iconsBar)
currentIconsBar
.querySelectorAll(".skill-icon[data-weapon]")
.forEach((el) => {
const weapon = el.dataset.weapon;
if (weapon && weapon.trim() !== "" && weapon !== "{}") {
try {
const weaponObj = JSON.parse(weapon);
if (
weaponObj &&
typeof weaponObj === "object" &&
Object.keys(weaponObj).length > 0
) {
if (!el.classList.contains("has-weapon-available")) {
el.classList.add("has-weapon-available");
}
if (!el.querySelector(".weapon-indicator")) {
const ind = document.createElement("div");
ind.className = "weapon-indicator";
el.appendChild(ind);
}
}
} catch (e) {
// Se não for JSON válido, não adiciona a classe
}
}
});
// Busca em subskills (dentro de subskills-rail)
document
.querySelectorAll(".subskills-rail .subicon[data-weapon]")
.forEach((el) => {
const weapon = el.dataset.weapon;
if (weapon && weapon.trim() !== "" && weapon !== "{}") {
try {
const weaponObj = JSON.parse(weapon);
if (
weaponObj &&
typeof weaponObj === "object" &&
Object.keys(weaponObj).length > 0
) {
if (!el.classList.contains("has-weapon-available")) {
el.classList.add("has-weapon-available");
}
}
} catch (e) {
// Se não for JSON válido, não adiciona a classe
}
}
});
}
// Aplica classes de weapon imediatamente ao carregar
reapplyWeaponClassesToBar();
// REMOVIDO: setupWeaponBarToggle - O toggle de weapon agora é criado apenas pelo C.WeaponToggle.html no header
// Não cria mais botão na barra de skills - apenas aplica classes visuais
onWeaponStateChange(syncWeaponRailState);
// Reaplica classes quando o estado do weapon muda (para garantir que funcione mesmo quando toggle é ativado fora da barra)
onWeaponStateChange(() => {
// Usa setTimeout para garantir que o DOM foi atualizado
setTimeout(() => {
reapplyWeaponClassesToBar();
}, 50);
});
syncWeaponRailState(globalWeaponEnabled);
(function injectWeaponStyles() {
if (document.getElementById("weapon-toggle-styles")) return;
const style = document.createElement("style");
style.id = "weapon-toggle-styles";
style.textContent = `
/* ========== ESTILOS DE WEAPON - NOVO SISTEMA ========== */
/* Animação simples para borda */
@keyframes weapon-border-flow {
0% { background-position: 0% 0%; }
100% { background-position: 200% 0%; }
}
/* Animação sutil para brilho */
@keyframes weapon-glow-breathe {
0%, 100% { opacity: 0.7; }
50% { opacity: 1; }
}
/* Animação do gradiente (sem girar a borda) */
@property --effect-spin {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@keyframes effect-border-spin {
from { --effect-spin: 0deg; }
to { --effect-spin: 360deg; }
}
/* ===== ÍCONE COM ARMA - Só mostra efeitos quando weapon-mode-on está ativo ===== */
/* Quando NÃO está em weapon-mode-on, os ícones com arma devem ser normais (mesma borda padrão) */
.character-box .top-rail.skills .icon-bar .skill-icon.has-weapon-available:not(.weapon-bar-toggle) {
/* Remove borda sólida base que aparece na transição do toggle */
border-color: transparent !important;
box-shadow: none !important;
}
.character-box .top-rail.skills:not(.weapon-mode-on) .icon-bar .skill-icon.has-weapon-available:not(.weapon-bar-toggle) {
/* Remove transform scale quando toggle está desativado */
transform: none !important;
transition: transform 0.15s ease !important;
}
.character-box .top-rail.skills:not(.weapon-mode-on) .icon-bar .skill-icon.has-weapon-available:not(.weapon-bar-toggle)::after {
/* Reseta COMPLETAMENTE para borda padrão quando toggle está desativado */
/* Remove background, padding e mask INSTANTANEAMENTE (sem transição) */
background: none !important;
background-size: unset !important;
padding: 0 !important;
-webkit-mask: none !important;
mask: none !important;
mask-composite: unset !important;
-webkit-mask-composite: unset !important;
animation: none !important;
/* Apenas box-shadow tem transição suave */
box-shadow: inset 0 0 0 var(--icon-ring-w) var(--icon-idle) !important;
/* SEM transição no background/padding/mask para evitar "flash" durante a mudança */
transition: box-shadow 0.15s ease !important;
}
.character-box .top-rail.skills:not(.weapon-mode-on) .icon-bar .skill-icon.has-weapon-available:not(.weapon-bar-toggle)::before {
/* Remove efeitos de glow quando toggle está desativado - com transição suave */
opacity: 0 !important;
transition: opacity 0.15s ease !important;
}
/* Quando ativo mas toggle desativado, usa borda padrão de ativo */
.character-box .top-rail.skills:not(.weapon-mode-on) .icon-bar .skill-icon.has-weapon-available:not(.weapon-bar-toggle).active {
/* Mantém o zoom padrão mesmo quando toggle está desativado */
transform: scale(1.10) translateZ(0) !important;
transition: transform 0.15s ease !important;
}
.character-box .top-rail.skills:not(.weapon-mode-on) .icon-bar .skill-icon.has-weapon-available:not(.weapon-bar-toggle).active::after {
/* Reseta COMPLETAMENTE para borda padrão de ativo */
/* Remove background, padding e mask INSTANTANEAMENTE (sem transição) */
background: none !important;
background-size: unset !important;
padding: 0 !important;
-webkit-mask: none !important;
mask: none !important;
mask-composite: unset !important;
-webkit-mask-composite: unset !important;
animation: none !important;
/* Apenas box-shadow tem transição suave */
box-shadow: inset 0 0 0 var(--icon-ring-w) var(--icon-active) !important;
/* SEM transição no background/padding/mask para evitar "flash" durante a mudança */
transition: box-shadow 0.15s ease !important;
}
.character-box .top-rail.skills:not(.weapon-mode-on) .icon-bar .skill-icon.has-weapon-available:not(.weapon-bar-toggle).active::before {
/* Sem glow amarelo no modo normal para skills com weapon */
opacity: 0 !important;
box-shadow: none !important;
transition: opacity 0.15s ease !important;
}
/* ===== MODO WEAPON ON - INATIVO ===== */
.character-box .top-rail.skills.weapon-mode-on .icon-bar .skill-icon.has-weapon-available:not(.weapon-bar-toggle):not(.active)::after {
pointer-events: none !important;
box-shadow: none !important;
background: linear-gradient(90deg,
#FF3333 0%,
#FF0000 50%,
#FF3333 100%) !important;
background-size: 200% 100% !important;
animation: weapon-border-flow 3s 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;
/* SEM transição para permitir remoção instantânea quando toggle é desativado */
transition: none !important;
}
.character-box .top-rail.skills.weapon-mode-on .icon-bar .skill-icon.has-weapon-available:not(.weapon-bar-toggle):not(.active)::before {
pointer-events: none !important;
inset: 0 !important;
border-radius: inherit !important;
z-index: 1 !important;
animation: weapon-glow-breathe 2s ease-in-out infinite !important;
box-shadow:
0 0 8px 0 rgba(255, 0, 0, 0.4),
0 0 12px 0 rgba(255, 51, 51, 0.3),
0 0 16px 0 rgba(255, 0, 0, 0.2) !important;
opacity: 1 !important;
}
/* ===== MODO WEAPON ON - ATIVO ===== */
.character-box .top-rail.skills.weapon-mode-on .icon-bar .skill-icon.has-weapon-available:not(.weapon-bar-toggle).active {
transform: scale(1.10) !important;
z-index: 5 !important;
}
.character-box .top-rail.skills.weapon-mode-on .icon-bar .skill-icon.has-weapon-available:not(.weapon-bar-toggle).active::after {
pointer-events: none !important;
box-shadow: none !important;
background: linear-gradient(90deg,
#FFEB3B 0%,
#FFD700 50%,
#FFEB3B 100%) !important;
background-size: 200% 100% !important;
animation: weapon-border-flow 2s 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;
/* SEM transição para permitir remoção instantânea quando toggle é desativado */
transition: none !important;
}
.character-box .top-rail.skills.weapon-mode-on .icon-bar .skill-icon.has-weapon-available:not(.weapon-bar-toggle).active::before {
pointer-events: none !important;
inset: 0 !important;
border-radius: inherit !important;
z-index: 1 !important;
animation: weapon-glow-breathe 1.5s ease-in-out infinite !important;
box-shadow:
0 0 10px 0 rgba(255, 215, 0, 0.5),
0 0 16px 0 rgba(255, 235, 59, 0.4),
0 0 22px 0 rgba(255, 215, 0, 0.3) !important;
opacity: 1 !important;
}
.character-box .top-rail.skills .icon-bar {
position: relative;
}
.character-box .top-rail.skills .icon-bar .skill-icon {
z-index: 2;
}
.character-box .top-rail.skills .icon-bar .skill-icon.effect-active:not(.weapon-bar-toggle):not(.active)::after {
box-shadow:
inset 0 0 0 var(--icon-ring-w) rgba(210, 60, 60, 0.95),
inset 0 0 0 calc(var(--icon-ring-w) + 1px) rgba(120, 20, 20, 0.75) !important;
animation: effect-child-ring 1.6s ease-in-out infinite !important;
}
@keyframes effect-child-ring {
0% {
box-shadow:
inset 0 0 0 var(--icon-ring-w) rgba(210, 60, 60, 0.95),
inset 0 0 0 calc(var(--icon-ring-w) + 1px) rgba(120, 20, 20, 0.6);
}
50% {
box-shadow:
inset 0 0 0 var(--icon-ring-w) rgba(120, 20, 20, 0.95),
inset 0 0 0 calc(var(--icon-ring-w) + 1px) rgba(20, 0, 0, 0.95);
}
100% {
box-shadow:
inset 0 0 0 var(--icon-ring-w) rgba(210, 60, 60, 0.95),
inset 0 0 0 calc(var(--icon-ring-w) + 1px) rgba(120, 20, 20, 0.6);
}
}
.character-box .top-rail.skills .effect-lines-layer {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
overflow: visible;
}
.character-box .top-rail.skills .effect-lines-layer .effect-line {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.character-box .top-rail.skills .effect-lines-layer .effect-line-glow {
stroke: rgba(120, 20, 20, 0.7);
stroke-width: 5;
filter: drop-shadow(0 0 6px rgba(255, 60, 60, 0.45));
animation: effect-line-glow 1.6s ease-in-out infinite;
}
.character-box .top-rail.skills .effect-lines-layer .effect-line-core {
stroke: rgba(210, 60, 60, 0.95);
stroke-width: 2.2;
animation: effect-line-pulse 1.6s ease-in-out infinite;
}
.character-box .top-rail.skills .effect-lines-layer .effect-line.effect-line-returning {
animation: effect-line-return var(--effect-return-duration, 1.1s) ease forwards;
animation-delay: var(--effect-return-delay, 0s);
}
@keyframes effect-line-pulse {
0% {
stroke: rgba(210, 60, 60, 0.95);
stroke-width: 2.1;
}
50% {
stroke: rgba(120, 20, 20, 0.95);
stroke-width: 2.7;
}
100% {
stroke: rgba(220, 70, 70, 0.95);
stroke-width: 2.1;
}
}
@keyframes effect-line-glow {
0% {
stroke: rgba(140, 25, 25, 0.65);
filter: drop-shadow(0 0 5px rgba(255, 80, 80, 0.35));
}
50% {
stroke: rgba(20, 0, 0, 0.85);
filter: drop-shadow(0 0 9px rgba(255, 70, 70, 0.65));
}
100% {
stroke: rgba(140, 25, 25, 0.65);
filter: drop-shadow(0 0 5px rgba(255, 80, 80, 0.35));
}
}
@keyframes effect-line-return {
0% {
stroke-dasharray: var(--effect-line-length, 0) var(--effect-line-length, 0);
stroke-dashoffset: 0;
opacity: 1;
}
100% {
stroke-dasharray: 0 var(--effect-line-length, 0);
stroke-dashoffset: var(--effect-line-length, 0);
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.character-box .top-rail.skills .effect-lines-layer .effect-line {
animation: none;
}
}
.character-box .top-rail.skills.effect-mode-on .icon-bar .skill-icon.effect-active:not(.weapon-bar-toggle).active::before {
pointer-events: none !important;
inset: 0 !important;
border-radius: inherit !important;
z-index: 1 !important;
animation: weapon-glow-breathe 1.4s ease-in-out infinite !important;
box-shadow:
0 0 10px 0 rgba(220, 220, 220, 0.5),
0 0 16px 0 rgba(190, 190, 190, 0.4),
0 0 22px 0 rgba(220, 220, 220, 0.3) !important;
opacity: 1 !important;
}
/* ========== ESTILOS DE SWAP DE PERSONAGENS (Sistema Genérico) ========== */
/* Skills desabilitadas quando o personagem ativo não pode usá-las */
.character-box .top-rail.skills .icon-bar .skill-icon.disabled-skill {
opacity: 0.3 !important;
filter: grayscale(100%) !important;
cursor: not-allowed !important;
pointer-events: none !important;
transition: opacity 0.2s ease, filter 0.2s ease !important;
}
`;
document.head.appendChild(style);
})();
function applyWeaponBadge(el, weaponData, equipped) {
// Apenas gerencia a classe weapon-equipped (badge visual removido)
if (equipped && weaponData) {
el.classList.add("weapon-equipped");
} else {
el.classList.remove("weapon-equipped");
}
}
function getWeaponKey(el) {
return (
(el.dataset.index || "") +
":" +
(el.dataset.nome || el.dataset.name || "")
);
}
function isWeaponModeOn() {
try {
return localStorage.getItem("glaWeaponEnabled") === "1";
} catch (e) {
return false;
}
}
function getWeaponDataForIcon(iconEl) {
if (!iconEl || !iconEl.dataset.weapon) return null;
// Usa cache de parsing JSON
return getCachedJSON(iconEl, "weapon");
}
const effectState = {
skills: new Set(),
videos: new Map(),
expiresAt: 0,
sourceIcon: null,
timer: null,
};
let effectLinesLayer = null;
let effectLinesRAF = null;
let effectLinesCleanupTimer = null;
let effectLinesScrollWrap = null;
let effectLinesBound = false;
let effectLinesLastState = null;
function getSkillNameFromIcon(iconEl) {
return (iconEl?.dataset?.nome || iconEl?.dataset?.name || "").trim();
}
function getEffectVideoKey(iconEl, videoValue) {
if (!iconEl) return "";
const baseIndex = iconEl.dataset.index || "";
const subName =
iconEl.dataset.subName ||
iconEl.dataset.nome ||
iconEl.dataset.name ||
"";
const namePart = baseIndex || subName || "";
const videoPart = String(videoValue || "").trim();
if (!namePart || !videoPart) return "";
return `effect:${namePart}:${videoPart}`;
}
function normalizeEffectData(raw) {
if (!raw || typeof raw !== "object") return null;
const rawSkills = raw.skills;
const timeValue = Number(raw.time ?? raw.duration ?? 0);
const timeMs = Number.isFinite(timeValue) ? timeValue * 1000 : 0;
if (!timeMs || timeMs <= 0) return null;
const skills = Array.isArray(rawSkills)
? rawSkills.map((s) => String(s || "").trim()).filter(Boolean)
: [];
if (!skills.length) return null;
const videos = new Map();
if (raw.videos && typeof raw.videos === "object") {
Object.keys(raw.videos).forEach((k) => {
const key = String(k || "").trim();
const val = String(raw.videos[k] || "").trim();
if (key && val) videos.set(key, val);
});
}
return { skills, timeMs, videos };
}
function handleEffectLinesScroll() {
scheduleEffectLinesUpdate();
}
function bindEffectLinesEvents() {
if (!effectLinesBound) {
window.addEventListener("resize", scheduleEffectLinesUpdate);
window.addEventListener("scroll", scheduleEffectLinesUpdate, {
passive: true,
});
effectLinesBound = true;
}
const rail = document.querySelector(".top-rail.skills");
const scrollWrap = rail ? rail.querySelector(".icon-scroll-x") : null;
if (scrollWrap && scrollWrap !== effectLinesScrollWrap) {
if (effectLinesScrollWrap) {
effectLinesScrollWrap.removeEventListener(
"scroll",
handleEffectLinesScroll
);
}
scrollWrap.addEventListener("scroll", handleEffectLinesScroll, {
passive: true,
});
effectLinesScrollWrap = scrollWrap;
}
}
function scheduleEffectLinesUpdate() {
if (effectLinesRAF) return;
effectLinesRAF = requestAnimationFrame(() => {
effectLinesRAF = null;
updateEffectLines();
});
}
function getEffectLinesLayer() {
const rail = document.querySelector(".top-rail.skills");
if (!rail) return null;
const iconBar = rail.querySelector(".icon-bar");
if (!iconBar) return null;
if (effectLinesLayer && effectLinesLayer.isConnected) {
return effectLinesLayer;
}
const existing = iconBar.querySelector(".effect-lines-layer");
if (existing) {
effectLinesLayer = existing;
return effectLinesLayer;
}
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.classList.add("effect-lines-layer");
svg.setAttribute("aria-hidden", "true");
iconBar.insertBefore(svg, iconBar.firstChild);
effectLinesLayer = svg;
return effectLinesLayer;
}
function updateEffectLines() {
if (!effectState.sourceIcon || !effectState.sourceIcon.isConnected) {
clearEffectLines();
return;
}
if (effectState.expiresAt <= Date.now()) {
clearEffectLines();
return;
}
const layer = getEffectLinesLayer();
if (!layer) return;
const rail = layer.closest(".top-rail.skills");
if (!rail) return;
const iconBar = rail.querySelector(".icon-bar");
if (!iconBar) return;
bindEffectLinesEvents();
const barRect = iconBar.getBoundingClientRect();
const width = Math.max(1, Math.round(iconBar.scrollWidth || barRect.width));
const height = Math.max(1, Math.round(iconBar.clientHeight || barRect.height));
layer.style.left = "0px";
layer.style.top = "0px";
layer.setAttribute("width", String(width));
layer.setAttribute("height", String(height));
layer.setAttribute("viewBox", `0 0 ${width} ${height}`);
layer.classList.remove("effect-lines-returning");
const srcRect = effectState.sourceIcon.getBoundingClientRect();
const startX =
srcRect.left + srcRect.width / 2 - barRect.left + iconBar.scrollLeft;
const startY = srcRect.top + srcRect.height / 2 - barRect.top;
const allIcons = Array.from(
document.querySelectorAll(".icon-bar .skill-icon[data-index]")
).filter((icon) => !icon.classList.contains("weapon-bar-toggle"));
const targets = allIcons.filter((icon) => {
if (icon === effectState.sourceIcon) return false;
const name = getSkillNameFromIcon(icon);
return name && effectState.skills.has(name);
});
if (!targets.length) {
clearEffectLines();
return;
}
const frag = document.createDocumentFragment();
const baselinePadding = 10;
const baselineExtra = 12;
const baselineY = Math.max(
startY,
height - baselinePadding + baselineExtra
);
const targetPoints = targets.map((target) => {
const tgtRect = target.getBoundingClientRect();
const endX =
tgtRect.left + tgtRect.width / 2 - barRect.left + iconBar.scrollLeft;
const endY = tgtRect.top + tgtRect.height / 2 - barRect.top;
return { x: endX, y: endY };
});
const xs = targetPoints.map((p) => p.x);
const minX = Math.min(startX, ...xs);
const maxX = Math.max(startX, ...xs);
effectLinesLastState = {
startX,
startY,
baselineY,
targets: targetPoints.map((p) => ({
x: p.x,
y: p.y,
dist: Math.hypot(p.x - startX, p.y - startY),
})),
};
const dParts = [
`M ${startX} ${startY}`,
`L ${startX} ${baselineY}`,
`L ${minX} ${baselineY}`,
`L ${maxX} ${baselineY}`,
];
targetPoints.forEach((p) => {
dParts.push(`M ${p.x} ${baselineY} L ${p.x} ${p.y}`);
});
const d = dParts.join(" ");
const glow = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
glow.setAttribute("d", d);
glow.classList.add("effect-line", "effect-line-glow");
const core = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
core.setAttribute("d", d);
core.classList.add("effect-line", "effect-line-core");
frag.appendChild(glow);
frag.appendChild(core);
requestAnimationFrame(() => {
const length = Math.max(1, Math.round(core.getTotalLength()));
[core, glow].forEach((path) => {
path.style.setProperty("--effect-line-length", `${length}`);
path.style.strokeDasharray = `${length}`;
path.style.strokeDashoffset = "0";
});
});
layer.replaceChildren(frag);
}
function animateEffectLinesReturn() {
const layer =
(effectLinesLayer && effectLinesLayer.isConnected
? effectLinesLayer
: document.querySelector(".effect-lines-layer")) || null;
if (!layer || !effectLinesLastState) {
clearEffectLines();
return;
}
const { startX, startY, baselineY, targets } = effectLinesLastState;
const sortedTargets = [...targets].sort((a, b) => b.dist - a.dist);
const returnDuration = 1.1;
const returnStagger = 0.22;
const frag = document.createDocumentFragment();
sortedTargets.forEach((target, idx) => {
const d = [
`M ${target.x} ${target.y}`,
`L ${target.x} ${baselineY}`,
`L ${startX} ${baselineY}`,
`L ${startX} ${startY}`,
].join(" ");
const glow = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
glow.setAttribute("d", d);
glow.classList.add(
"effect-line",
"effect-line-glow",
"effect-line-returning"
);
glow.style.setProperty("--effect-return-duration", `${returnDuration}s`);
glow.style.setProperty("--effect-return-delay", `${idx * returnStagger}s`);
const core = document.createElementNS(
"http://www.w3.org/2000/svg",
"path"
);
core.setAttribute("d", d);
core.classList.add(
"effect-line",
"effect-line-core",
"effect-line-returning"
);
core.style.setProperty("--effect-return-duration", `${returnDuration}s`);
core.style.setProperty("--effect-return-delay", `${idx * returnStagger}s`);
frag.appendChild(glow);
frag.appendChild(core);
});
layer.replaceChildren(frag);
requestAnimationFrame(() => {
const paths = Array.from(layer.querySelectorAll("path.effect-line"));
paths.forEach((path) => {
const length = Math.max(1, Math.round(path.getTotalLength()));
path.style.setProperty("--effect-line-length", `${length}`);
path.style.strokeDasharray = `${length} ${length}`;
path.style.strokeDashoffset = "0";
});
});
if (effectLinesCleanupTimer) {
clearTimeout(effectLinesCleanupTimer);
}
effectLinesCleanupTimer = setTimeout(() => {
clearEffectLines();
}, (returnDuration + returnStagger * Math.max(0, targets.length - 1)) * 1000 + 120);
}
function clearEffectLines() {
if (effectLinesCleanupTimer) {
clearTimeout(effectLinesCleanupTimer);
effectLinesCleanupTimer = null;
}
const layer =
(effectLinesLayer && effectLinesLayer.isConnected
? effectLinesLayer
: document.querySelector(".effect-lines-layer")) || null;
if (layer) layer.remove();
effectLinesLayer = null;
}
function applyEffectClasses() {
const isActive = effectState.expiresAt > Date.now();
const rail = document.querySelector(".top-rail.skills");
if (rail) rail.classList.toggle("effect-mode-on", isActive);
document
.querySelectorAll(".icon-bar .skill-icon[data-index]")
.forEach((icon) => {
if (icon.classList.contains("weapon-bar-toggle")) return;
const name = getSkillNameFromIcon(icon);
const should =
isActive && name && effectState.skills.has(name);
icon.classList.toggle("effect-active", !!should);
if (!should) {
icon.style.removeProperty("--effect-strength");
}
});
if (isActive) {
bindEffectLinesEvents();
scheduleEffectLinesUpdate();
}
}
function clearEffectState() {
const activeIcon = document.querySelector(
".icon-bar .skill-icon.active"
);
const activeName = getSkillNameFromIcon(activeIcon);
const wasAffected =
activeName && effectState.skills.has(activeName);
animateEffectLinesReturn();
if (effectState.timer) {
clearTimeout(effectState.timer);
effectState.timer = null;
}
effectState.skills.clear();
effectState.videos.clear();
effectState.expiresAt = 0;
effectState.sourceIcon = null;
applyEffectClasses();
if (wasAffected && activeIcon) {
activeIcon.dispatchEvent(new Event("click", { bubbles: true }));
}
}
function activateEffectFromIcon(iconEl) {
const effectRaw = getCachedJSON(iconEl, "effect");
const normalized = normalizeEffectData(effectRaw);
if (!normalized) return;
effectState.skills = new Set(normalized.skills);
effectState.videos = normalized.videos;
effectState.expiresAt = Date.now() + normalized.timeMs;
effectState.sourceIcon = iconEl;
if (effectState.timer) clearTimeout(effectState.timer);
effectState.timer = setTimeout(() => {
clearEffectState();
}, normalized.timeMs + 5);
applyEffectClasses();
}
function getEffectVideoForIcon(iconEl) {
if (!iconEl) return "";
if (effectState.expiresAt <= Date.now()) return "";
const name = getSkillNameFromIcon(iconEl);
if (!name || !effectState.skills.has(name)) return "";
if (effectState.videos.has(name)) return effectState.videos.get(name) || "";
return "";
}
function getEffectiveSkillVideoFromIcon(iconEl) {
const weaponOn = globalWeaponEnabled;
const weaponData = getWeaponDataForIcon(iconEl);
const baseVideoFile = (iconEl.dataset.videoFile || "").trim();
const baseVideoURL = (iconEl.dataset.video || "").trim();
// console.log('[Skills DEBUG]', {
// skillName: iconEl.dataset.nome || iconEl.dataset.name,
// weaponOn,
// hasWeaponData: !!weaponData,
// weaponData: weaponData,
// baseVideoFile,
// baseVideoURL
// });
const effectVideo = getEffectVideoForIcon(iconEl);
if (effectVideo && effectVideo.trim() !== "") {
return effectVideo.trim();
}
if (weaponOn && weaponData) {
// console.log('[Skills] checking weapon video', {
// skillName: iconEl.dataset.nome || iconEl.dataset.name,
// hasVideo: !!(weaponData.video),
// videoValue: weaponData.video,
// videoTrimmed: weaponData.video ? weaponData.video.trim() :
// });
if (weaponData.video && weaponData.video.trim() !== "") {
// console.log('[Skills] video escolhido (weapon)', iconEl.dataset.nome || iconEl.dataset.name, weaponData.video);
return weaponData.video.trim();
}
}
// Sistema genérico de swap: verifica character_videos se houver personagem ativo
if (activeCharacter !== null && iconEl.dataset.characterVideos) {
try {
const characterVideos = JSON.parse(iconEl.dataset.characterVideos);
const characterVideo = characterVideos[activeCharacter];
if (characterVideo && characterVideo.trim() !== "") {
return characterVideo.trim();
}
} catch (e) {
console.warn("[Swap] Erro ao processar character_videos:", e);
}
}
// Prioriza videoFile (nome do arquivo), mas se estiver vazio, usa video (pode ser URL completa)
const result = baseVideoFile || baseVideoURL || "";
// console.log('[Skills] video escolhido (base)', iconEl.dataset.nome || iconEl.dataset.name, result);
return result;
}
function createVideoElement(videoURL, extraAttrs = {}) {
// Se o vídeo já falhou antes, não cria novo elemento
if (failedVideosCache.has(videoURL)) {
return null;
}
const v = document.createElement("video");
v.className = "skill-video";
v.setAttribute("controls", "");
v.setAttribute("preload", "auto"); // Mudado de 'metadata' para 'auto' para carregar tudo imediatamente
v.setAttribute("playsinline", "");
v.style.display = "none";
v.style.width = "100%";
v.style.height = "auto";
v.style.aspectRatio = "16/9";
v.style.objectFit = "cover";
Object.keys(extraAttrs).forEach((k) => {
v.dataset[k] = extraAttrs[k];
});
// 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");
// Tratamento silencioso de erros - marca como falhado e não tenta mais
let errorHandled = false;
v.addEventListener(
"error",
(e) => {
if (errorHandled) return;
errorHandled = true;
// Marca o vídeo como falhado para não tentar carregar novamente
failedVideosCache.add(videoURL);
// Remove o vídeo do DOM se estiver lá
if (v.parentNode) {
v.parentNode.removeChild(v);
}
// Avisa apenas uma vez sobre vídeos faltantes
if (!missingVideosReported.has(videoURL)) {
missingVideosReported.add(videoURL);
// Extrai nome do arquivo da URL para o aviso
const fileName =
videoURL.split("/").pop().split("?")[0] || videoURL;
console.info(
`[Skills] Vídeo não encontrado na wiki: ${decodeURIComponent(
fileName
)}. Este aviso aparecerá apenas uma vez.`
);
}
},
{ once: true }
);
return v; }
// Função recursiva para carregar TODOS os vídeos de subskills (incluindo sub-subskills)
function preloadSubskillVideosRecursively(
subs,
parentIdx,
parentPath = ""
) {
if (!videoBox || !Array.isArray(subs)) return 0;
let createdCount = 0;
subs.forEach((s) => {
const subName = (s.name || s.n || "").trim();
const currentPath = parentPath ? `${parentPath}:${subName}` : subName;
// Vídeo normal da subskill
if (s.video && s.video.trim() !== "") {
const key = `sub:${parentIdx}:${currentPath}`;
if (!subskillVideosCache.has(key)) {
const videoURL = normalizeFileURL(s.video);
if (
videoURL &&
videoURL.trim() !== "" &&
!failedVideosCache.has(videoURL)
) {
const v = createVideoElement(videoURL, {
sub: "1",
parentIndex: parentIdx,
subName: currentPath,
cacheKey: key,
});
if (v) {
// Só adiciona se o vídeo foi criado (não estava no cache de falhas)
videoBox.appendChild(v);
subskillVideosCache.set(key, v);
createdCount++;
// FORÇA carregamento imediatamente (apenas uma vez)
v.load();
}
}
}
}
// Vídeo de weapon da subskill (sempre carrega, independente do toggle)
if (
s.weapon &&
typeof s.weapon === "object" &&
s.weapon.video &&
s.weapon.video.trim() !== ""
) {
const weaponKey = `sub:${parentIdx}:${currentPath}:weapon`;
if (!subskillVideosCache.has(weaponKey)) {
const weaponVideoURL = normalizeFileURL(s.weapon.video);
if (
weaponVideoURL &&
weaponVideoURL.trim() !== "" &&
!failedVideosCache.has(weaponVideoURL)
) {
const v = createVideoElement(weaponVideoURL, {
sub: "1",
parentIndex: parentIdx,
subName: currentPath,
weapon: "1",
cacheKey: weaponKey,
});
if (v) {
// Só adiciona se o vídeo foi criado (não estava no cache de falhas)
videoBox.appendChild(v);
subskillVideosCache.set(weaponKey, v);
createdCount++;
// FORÇA carregamento imediatamente (apenas uma vez)
v.load();
}
}
}
}
// RECURSÃO: processa sub-subskills (SEMPRE processa, mesmo se o vídeo já estiver no cache)
if (Array.isArray(s.subs) && s.subs.length > 0) {
createdCount += preloadSubskillVideosRecursively(
s.subs,
parentIdx,
currentPath
);
}
});
return createdCount;
}
function precreateSubskillVideos() {
if (!videoBox) return;
let createdCount = 0;
iconItems.forEach((parentIcon) => {
const subs = getCachedJSON(parentIcon, "subs");
if (!Array.isArray(subs)) return;
const parentIdx = parentIcon.dataset.index || "";
createdCount += preloadSubskillVideosRecursively(subs, parentIdx);
});
}
// Função para pré-carregar TODOS os vídeos recursivamente (principais, subskills, weapon, sub-subskills)
function preloadAllVideosRecursively() {
if (!videoBox || !iconItems.length) return;
// 1. Carregar vídeos de skills principais
iconItems.forEach((el) => {
const idx = el.dataset.index || "";
if (!idx) return;
// Vídeo normal da skill principal
// Prioriza data-video-file (nome do arquivo), depois data-video (pode ser URL completa)
let src = (el.dataset.videoFile || "").trim();
if (!src) {
// Se não tem videoFile, tenta extrair do video (pode ser URL completa)
const videoAttr = (el.dataset.video || "").trim();
if (videoAttr) {
// Se já é uma URL completa, usa direto; senão normaliza
if (videoAttr.includes("/") || videoAttr.startsWith("http")) {
src = videoAttr;
} else {
src = videoAttr;
}
}
}
if (src && !videosCache.has(idx)) {
const videoURL = normalizeFileURL(src);
if (videoURL && !failedVideosCache.has(videoURL)) {
const v = createVideoElement(videoURL, {
index: idx,
});
if (v) {
// Só adiciona se o vídeo foi criado (não estava no cache de falhas)
totalVideos++;
v.style.maxWidth = "100%";
v.addEventListener(
"canplaythrough",
() => {
loadedVideos++;
if (!userHasInteracted && loadedVideos === 1) {
try {
v.pause();
v.currentTime = 0;
} catch (e) { }
}
if (loadedVideos === totalVideos) autoplay = true;
},
{
once: true,
}
);
v.addEventListener(
"error",
() => {
loadedVideos++;
if (loadedVideos === totalVideos) autoplay = true;
},
{
once: true,
}
);
videoBox.appendChild(v);
videosCache.set(idx, v);
nestedVideoElByIcon.set(el, v);
// Força carregamento imediatamente (apenas uma vez)
v.load();
}
}
}
// Vídeo de weapon (sempre carrega, independente do toggle)
const weaponData = getCachedJSON(el, "weapon");
if (
weaponData &&
weaponData.video &&
weaponData.video.trim() !== ""
) {
const weaponKey = `weapon:${idx}:${(
el.dataset.nome ||
el.dataset.name ||
""
).trim()}`;
if (!videosCache.has(weaponKey)) {
const weaponVideoURL = normalizeFileURL(weaponData.video);
if (weaponVideoURL && !failedVideosCache.has(weaponVideoURL)) {
const v = createVideoElement(weaponVideoURL, {
index: idx,
weapon: "1",
});
if (v) {
// Só adiciona se o vídeo foi criado (não estava no cache de falhas)
totalVideos++;
v.style.maxWidth = "100%";
v.addEventListener(
"canplaythrough",
() => {
loadedVideos++;
if (loadedVideos === totalVideos) autoplay = true;
},
{
once: true,
}
);
v.addEventListener(
"error",
() => {
loadedVideos++;
if (loadedVideos === totalVideos) autoplay = true;
},
{
once: true,
}
);
videoBox.appendChild(v);
videosCache.set(weaponKey, v);
// Força carregamento imediatamente (apenas uma vez)
v.load();
}
}
}
}
});
// 2. Carregar vídeos de subskills (recursivamente)
precreateSubskillVideos();
}
// Função para pré-carregar TODOS os ícones recursivamente
function preloadAllIconsRecursively() {
const iconCache = new Set();
// Função recursiva para processar subskills
function processSubskillsRecursively(subs) {
if (!Array.isArray(subs)) return;
subs.forEach((s) => {
const icon = (s.icon || "").trim();
if (icon) {
const iconURL = normalizeFileURL(icon);
if (iconURL && !iconCache.has(iconURL)) {
iconCache.add(iconURL);
const img = new Image();
img.decoding = "async";
img.loading = "eager";
img.referrerPolicy = "same-origin";
img.src = iconURL;
imagePreloadCache.set(iconURL, img);
}
}
// Recursão para sub-subskills
if (Array.isArray(s.subs)) {
processSubskillsRecursively(s.subs);
}
});
}
// Carregar ícones de skills principais
iconItems.forEach((el) => {
const img = el.querySelector("img");
if (img && img.src) {
const iconURL = img.src;
if (iconURL && !iconCache.has(iconURL)) {
iconCache.add(iconURL);
// Já está no DOM, mas força pré-carregamento
const preloadImg = new Image();
preloadImg.decoding = "async";
preloadImg.loading = "eager";
preloadImg.referrerPolicy = "same-origin";
preloadImg.src = iconURL;
imagePreloadCache.set(iconURL, preloadImg);
}
}
// Carregar ícones de subskills recursivamente
const subs = getCachedJSON(el, "subs");
if (Array.isArray(subs)) {
processSubskillsRecursively(subs);
}
});
}
// Função principal que carrega TUDO imediatamente
function preloadAllAssets() {
// Carregar TUDO imediatamente - sem lazy loading
// 1. Carregar TODOS os vídeos (principais, subskills, weapon, sub-subskills)
preloadAllVideosRecursively();
// 2. Carregar TODOS os ícones (principais, subskills, sub-subskills)
preloadAllIconsRecursively();
}
// Executa pré-carregamento imediatamente
preloadAllAssets();
function wireTooltipsForNewIcons() {
const tip = document.querySelector(".skill-tooltip");
if (!tip) return;
let lockUntil2 = 0;
Array.from(document.querySelectorAll(".icon-bar .skill-icon")).forEach(
(icon) => {
if (
icon.dataset.weaponToggle === "1" ||
icon.classList.contains("weapon-bar-toggle")
)
return;
if (icon.dataset.tipwired) return;
icon.dataset.tipwired = "1";
const label =
icon.dataset.nome || icon.dataset.name || icon.title || "";
if (label && !icon.hasAttribute("aria-label"))
icon.setAttribute("aria-label", label);
if (icon.hasAttribute("title")) icon.removeAttribute("title");
const img = icon.querySelector("img");
if (img) {
const imgAlt = img.getAttribute("alt") || "";
const imgTitle = img.getAttribute("title") || "";
if (!label && (imgAlt || imgTitle))
icon.setAttribute("aria-label", imgAlt || imgTitle);
img.setAttribute("alt", "");
if (img.hasAttribute("title")) img.removeAttribute("title");
}
const measureAndPos = (el) => {
if (!el || tip.getAttribute("aria-hidden") === "true") return;
tip.style.left = "0px";
tip.style.top = "0px";
const rect = el.getBoundingClientRect();
const tr = tip.getBoundingClientRect();
let left = Math.round(rect.left + (rect.width - tr.width) / 2);
left = Math.max(
8,
Math.min(left, window.innerWidth - tr.width - 8)
);
const coarse =
(window.matchMedia && matchMedia("(pointer: coarse)").matches) ||
window.innerWidth <= 600;
let top = coarse
? Math.round(rect.bottom + 10)
: Math.round(rect.top - tr.height - 8);
if (top < 8) top = Math.round(rect.bottom + 10);
tip.style.left = left + "px";
tip.style.top = top + "px";
};
const show = (el, text) => {
tip.textContent = text || "";
tip.setAttribute("aria-hidden", "false");
measureAndPos(el);
tip.style.opacity = "1";
};
const hide = () => {
tip.setAttribute("aria-hidden", "true");
tip.style.opacity = "0";
tip.style.left = "-9999px";
tip.style.top = "-9999px";
};
icon.addEventListener("mouseenter", () =>
show(icon, icon.dataset.nome || icon.dataset.name || "")
);
icon.addEventListener("mousemove", () => {
if (performance.now() >= lockUntil2) measureAndPos(icon);
});
icon.addEventListener("click", () => {
lockUntil2 = performance.now() + 240;
measureAndPos(icon);
});
icon.addEventListener("mouseleave", hide);
}
);
}
function showVideoForIcon(el) {
userHasInteracted = true;
if (!videoBox) return;
const effectiveVideo = getEffectiveSkillVideoFromIcon(el);
if (!effectiveVideo || effectiveVideo.trim() === "") {
videoBox.style.display = "none";
return;
}
const videoURL = normalizeFileURL(effectiveVideo);
if (!videoURL || videoURL.trim() === "") {
videoBox.style.display = "none";
return;
}
Array.from(videoBox.querySelectorAll("video.skill-video")).forEach(
(v) => {
try {
v.pause();
} catch (e) { }
v.style.display = "none";
}
);
if (window.__subskills) window.__subskills.hideAll?.(videoBox);
const hasIdx = !!el.dataset.index;
const weaponOn = globalWeaponEnabled;
const weaponData = getWeaponDataForIcon(el);
const isWeaponVideo =
weaponOn &&
weaponData &&
weaponData.video &&
weaponData.video.trim() !== "";
const effectVideo = getEffectVideoForIcon(el);
const isEffectVideo = !!(effectVideo && effectVideo.trim() !== "");
const effectKey = isEffectVideo
? getEffectVideoKey(el, effectiveVideo)
: "";
// console.log('[Skills] showVideoForIcon chamado', {
// skillName: el.dataset.nome || el.dataset.name,
// weaponOn,
// isWeaponVideo,
// effectiveVideo: getEffectiveSkillVideoFromIcon(el)
// });
const videoKey = isWeaponVideo
? `weapon:${getWeaponKey(el)}`
: isEffectVideo
? effectKey
: el.dataset.index || "";
const isSubskill = !hasIdx || el.dataset.nested === "1";
const parentIdx = el.dataset.parentIndex || "";
const subName =
el.dataset.subName || el.dataset.nome || el.dataset.name || "";
if (
hasIdx &&
!isWeaponVideo &&
!isEffectVideo &&
videosCache.has(el.dataset.index)
) {
const v = videosCache.get(el.dataset.index);
videoBox.style.display = "block";
v.style.display = "block";
try {
v.currentTime = 0;
} catch (e) { }
const suppress = document.body.dataset.suppressSkillPlay === "1";
if (!suppress) {
v.play().catch(() => { });
} else {
try {
v.pause();
} catch (e) { }
}
return;
}
// Para form_switch, permite criação dinâmica de vídeos (transições de forma)
// Vídeos normais devem estar pré-carregados
const isFormSwitch =
el.dataset.formSwitch === "true" ||
el.getAttribute("data-form-switch") === "true";
let v = null;
if (isWeaponVideo) {
const weaponKeyFull = `weapon:${getWeaponKey(el)}`;
v = videosCache.get(weaponKeyFull);
if (!v && isSubskill && parentIdx && subName) {
// Tenta buscar vídeo de weapon de subskill
// O cache usa o formato: sub:${parentIdx}:${path}:weapon onde path NÃO inclui o nome da skill principal
// O subName agora já está no formato correto (sem nome da skill principal)
// Tenta buscar diretamente com o subName
let subWeaponKey = `sub:${parentIdx}:${subName}:weapon`;
v = subskillVideosCache.get(subWeaponKey);
// Se não encontrou, tenta buscar recursivamente
if (!v) {
const basePattern = `sub:${parentIdx}:`;
const weaponSuffix = ":weapon";
const searchName = subName.split(":").pop();
for (const [key, video] of subskillVideosCache.entries()) {
if (key.startsWith(basePattern) && key.endsWith(weaponSuffix)) {
const pathInKey = key.substring(
basePattern.length,
key.length - weaponSuffix.length
);
// Tenta match exato, ou se termina com o nome da skill
if (
pathInKey === subName ||
pathInKey.endsWith(`:${searchName}`) ||
pathInKey === searchName
) {
v = video;
break;
}
}
}
}
}
if (!v) {
// Tenta buscar pelo padrão antigo também
v = videoBox.querySelector(`video[data-weapon-key="${videoKey}"]`);
}
} else if (isEffectVideo && effectKey) {
v = videosCache.get(effectKey);
} else {
if (isSubskill && parentIdx && subName) {
// Busca vídeo de subskill no cache correto
// O cache usa o formato: sub:${parentIdx}:${path} onde path NÃO inclui o nome da skill principal
// O subName agora já está no formato correto (sem nome da skill principal)
// Tenta buscar diretamente com o subName
let subKey = `sub:${parentIdx}:${subName}`;
v = subskillVideosCache.get(subKey);
// Se não encontrou, tenta buscar recursivamente todas as chaves que começam com o padrão
// Isso é útil caso haja alguma diferença no formato do caminho
if (!v) {
const basePattern = `sub:${parentIdx}:`;
const searchName = subName.split(":").pop(); // Último segmento do caminho
// Tenta também buscar apenas pelo último segmento (útil para sub-subskills)
const lastSegmentKey = `sub:${parentIdx}:${searchName}`;
v = subskillVideosCache.get(lastSegmentKey);
// Se ainda não encontrou, faz busca mais ampla
if (!v) {
for (const [key, video] of subskillVideosCache.entries()) {
if (key.startsWith(basePattern)) {
const pathInKey = key.substring(basePattern.length);
// Tenta match exato, ou se termina com o nome da skill, ou se contém o caminho completo
if (
pathInKey === subName ||
pathInKey.endsWith(`:${searchName}`) ||
pathInKey === searchName ||
(subName.includes(":") && pathInKey.includes(subName)) ||
(subName.includes(":") &&
pathInKey.endsWith(
subName.split(":").slice(-2).join(":")
))
) {
v = video;
break;
}
}
}
}
}
} else {
v = videosCache.get(el.dataset.index);
if (!v) {
v = nestedVideoElByIcon.get(el);
}
}
}
// Se vídeo não foi encontrado no cache, verifica se falhou antes
if (!v) {
// Se o vídeo já foi marcado como falhado, não mostra aviso repetido
if (failedVideosCache.has(videoURL)) {
videoBox.style.display = "none";
return;
}
// Para effect, cria vídeo dinamicamente (vídeo alternativo por tempo)
if (isEffectVideo && videoURL) {
const baseIndex = el.dataset.index || "";
const effectCacheKey =
effectKey || `effect:${effectiveVideo || videoURL}`;
v = videosCache.get(effectCacheKey);
if (!v) {
v = createVideoElement(videoURL, {
index: baseIndex,
effect: "1",
});
if (v) {
videoBox.appendChild(v);
videosCache.set(effectCacheKey, v);
v.load();
} else {
videoBox.style.display = "none";
return;
}
}
}
// Para form_switch, cria vídeo dinamicamente (transições de forma são dinâmicas)
// Usa uma chave única que inclui o nome do arquivo do vídeo para garantir que cada vídeo diferente seja cacheado separadamente
else if (isFormSwitch && videoURL) {
const baseIndex = el.dataset.index || "";
const videoFileName = effectiveVideo || "";
// Chave única: index + nome do arquivo do vídeo
const videoKey = baseIndex + ":" + videoFileName;
v = videosCache.get(videoKey);
if (!v) {
v = createVideoElement(videoURL, { index: baseIndex });
if (v) {
videoBox.appendChild(v);
videosCache.set(videoKey, v);
v.load();
} else {
videoBox.style.display = "none";
return;
}
}
} else {
// Vídeos normais devem estar pré-carregados
videoBox.style.display = "none";
return;
}
}
videoBox.style.display = "block";
v.style.display = "block";
try {
v.currentTime = 0;
} catch (e) { }
const suppress = document.body.dataset.suppressSkillPlay === "1";
if (!suppress) {
v.play().catch(() => { });
} else {
try {
v.pause();
} catch (e) { }
}
}
function activateSkill(el, options = {}) {
const { openSubs = true } = options;
const tip = document.querySelector(".skill-tooltip");
if (tip) {
tip.setAttribute("aria-hidden", "true");
tip.style.opacity = "0";
tip.style.left = "-9999px";
tip.style.top = "-9999px";
}
const skillsRoot = document.getElementById("skills");
const i18nMap = skillsRoot
? JSON.parse(skillsRoot.dataset.i18nAttrs || "{}")
: {};
const L = i18nMap[getLangKey()] ||
i18nMap.pt || {
cooldown: "Recarga",
energy_gain: "Ganho de energia",
energy_cost: "Custo de energia",
power: "Poder",
power_pvp: "Poder PvP",
level: "Nível",
};
const name = el.dataset.nome || el.dataset.name || "";
if (el.dataset.effect) {
activateEffectFromIcon(el);
}
let weaponData = null;
if (el.dataset.weapon) {
try {
const parsed = JSON.parse(el.dataset.weapon);
// Só considera weapon válido se for um objeto não vazio
if (
parsed &&
typeof parsed === "object" &&
Object.keys(parsed).length > 0
) {
weaponData = parsed;
}
} catch (e) {
weaponData = null;
}
}
const hasWeapon = !!weaponData;
const weaponEquipped = hasWeapon && globalWeaponEnabled;
// Level: usa o level da weapon se estiver ativa e tiver level, senão usa o level da skill base
let level = (el.dataset.level || "").trim();
if (weaponEquipped && weaponData) {
// Verifica se weapon tem level definido (pode ser número ou string)
const weaponLevel = weaponData.level;
if (
weaponLevel !== undefined &&
weaponLevel !== null &&
weaponLevel !== ""
) {
level = weaponLevel.toString().trim();
}
}
const lang = getLangKey();
const baseDescPack = {
pt: el.dataset.descPt || "",
en: el.dataset.descEn || "",
es: el.dataset.descEs || "",
pl: el.dataset.descPl || "",
};
const baseDesc =
baseDescPack[lang] ||
baseDescPack.pt ||
baseDescPack.en ||
baseDescPack.es ||
baseDescPack.pl ||
el.dataset.desc ||
"";
// Aceita tanto desc_i18n quanto desc para compatibilidade
let weaponDescPack = {};
if (weaponData) {
if (weaponData.desc_i18n) {
weaponDescPack = weaponData.desc_i18n;
} else if (weaponData.desc) {
weaponDescPack = weaponData.desc;
} else {
weaponDescPack = {
pt: weaponData.descPt || "",
en: weaponData.descEn || "",
es: weaponData.descEs || "",
pl: weaponData.descPl || "",
};
}
}
const weaponDesc =
weaponDescPack[lang] ||
weaponDescPack.pt ||
weaponDescPack.en ||
weaponDescPack.es ||
weaponDescPack.pl ||
"";
const chosenDesc = weaponEquipped && weaponDesc ? weaponDesc : baseDesc;
const descHtml = chosenDesc.replace(/(.*?)/g, "$1");
let attrsHTML = "";
if (weaponEquipped && weaponData) {
// Faz merge: usa atributos da skill base e substitui apenas os que existem no weapon
// Parse dos atributos da skill base (formato: "pve, pvp, energy, cooldown" ou "pve, pvp, energy, cooldown")
const baseAttrsStr = el.dataset.atr || "";
// Suporta tanto ", " quanto "," como separador
const baseAttrs = baseAttrsStr.split(/\s*,\s*/).map((a) => a.trim());
const basePve =
baseAttrs[0] && baseAttrs[0] !== "-" ? baseAttrs[0] : "";
const basePvp =
baseAttrs[1] && baseAttrs[1] !== "-" ? baseAttrs[1] : "";
const baseEnergy =
baseAttrs[2] && baseAttrs[2] !== "-" ? baseAttrs[2] : "";
const baseCd = baseAttrs[3] && baseAttrs[3] !== "-" ? baseAttrs[3] : "";
// Valores do weapon (substituem os da skill base se existirem)
const wPve =
weaponData.powerpve !== undefined &&
weaponData.powerpve !== null &&
weaponData.powerpve !== ""
? weaponData.powerpve.toString().trim()
: basePve;
const wPvp =
weaponData.powerpvp !== undefined &&
weaponData.powerpvp !== null &&
weaponData.powerpvp !== ""
? weaponData.powerpvp.toString().trim()
: basePvp;
const wEnergy =
weaponData.energy !== undefined &&
weaponData.energy !== null &&
weaponData.energy !== ""
? weaponData.energy.toString().trim()
: baseEnergy;
const wCd =
weaponData.cooldown !== undefined &&
weaponData.cooldown !== null &&
weaponData.cooldown !== ""
? weaponData.cooldown.toString().trim()
: baseCd;
// Monta string de atributos mesclados
const mergedAttrs = [wPve, wPvp, wEnergy, wCd].join(",");
attrsHTML = renderAttributes(mergedAttrs);
} else {
attrsHTML = el.dataset.atr
? renderAttributes(el.dataset.atr)
: el.dataset.subattrs
? renderSubAttributesFromObj(JSON.parse(el.dataset.subattrs), L)
: "";
}
let flagsHTML = "";
// Debug: verifica se é uma skill do Urouge (verifica pela URL da página ou pelo contexto)
const isUrougePage =
window.location.href &&
window.location.href.toLowerCase().includes("urouge");
if (el.dataset.flags) {
try {
const flags = JSON.parse(el.dataset.flags);
if (flags && Array.isArray(flags) && flags.length > 0) {
flagsHTML = renderFlagsRow(flags);
// Debug para Urouge
if (isUrougePage) {
console.log("[Skills] Urouge - Flags processadas:", {
skill: name || el.dataset.nome,
flags: flags,
htmlLength: flagsHTML ? flagsHTML.length : 0,
hasHTML: !!flagsHTML,
});
}
} else {
if (isUrougePage) {
console.warn("[Skills] Urouge - Flags inválidas:", {
skill: name || el.dataset.nome,
flags: flags,
rawData: el.dataset.flags,
});
}
}
} catch (e) {
console.warn(
"[Skills] Erro ao processar flags:",
e,
"flags data:",
el.dataset.flags,
"skill:",
name || el.dataset.nome
);
if (isUrougePage) {
console.error(
"[Skills] Urouge - Erro ao processar flags:",
e,
"element:",
el
);
}
}
} else {
// Debug: verifica se deveria ter flags mas não tem (apenas para Urouge)
if (isUrougePage) {
console.warn("[Skills] Urouge - Skill sem data-flags:", {
skill: name || el.dataset.nome,
element: el,
allAttributes: Array.from(el.attributes).map(
(attr) => attr.name + "=" + attr.value
),
});
}
}
if (descBox) {
descBox.innerHTML = `
${name}
${level ? `
${L.level} ${level}
`
: ""
}${attrsHTML}
${descHtml}
`;
}
if (hasWeapon) {
applyWeaponBadge(el, weaponData, weaponEquipped);
}
if (videoBox) {
const oldFlags = videoBox.querySelector(".skill-flags");
if (oldFlags) oldFlags.remove();
if (flagsHTML) {
videoBox.insertAdjacentHTML("beforeend", flagsHTML);
applyFlagTooltips(videoBox);
}
}
const currIcons = Array.from(iconsBar.querySelectorAll(".skill-icon"));
currIcons.forEach((i) => i.classList.remove("active"));
const subsRaw = el.dataset.subs || el.getAttribute("data-subs");
const isFormSwitch =
el.dataset.formSwitch === "true" ||
el.getAttribute("data-form-switch") === "true";
const isSwap =
el.dataset.swap === "true" || el.getAttribute("data-swap") === "true";
// Se for skill de swap, troca personagem (não marca como ativo, não processa como back)
if (isSwap && !isFormSwitch) {
handleSwapCharacter(el);
// Não marca como ativo (similar ao form_switch)
return;
}
// Não marca como ativo se for form_switch (Change Form)
if (!isFormSwitch) {
el.classList.add("active");
if (!autoplay && loadedVideos > 0) autoplay = true;
window.__lastActiveSkillIcon = el;
// Lógica de vídeo: usa função centralizada que já considera weapon
showVideoForIcon(el);
}
const isBack =
el.dataset.back === "true" ||
el.getAttribute("data-back") === "true" ||
el.dataset.back === "yes" ||
el.getAttribute("data-back") === "yes" ||
el.dataset.back === "1" ||
el.getAttribute("data-back") === "1";
// Se for form_switch, alterna forma (não processa como back)
if (isFormSwitch) {
// Atualiza o data-video-file do ícone com o vídeo correto da transição baseado na forma atual
try {
const formsJSON = skillsRoot.dataset.forms || "{}";
if (formsJSON && formsJSON !== "{}") {
const tempFormsData = JSON.parse(formsJSON);
const formNames = Object.keys(tempFormsData);
// Busca vídeo de transição na skill Change Form
const formVideosRaw =
el.dataset.formVideos || el.getAttribute("data-form-videos");
if (formVideosRaw) {
const videos = JSON.parse(formVideosRaw);
// Se currentForm é null, detecta qual forma está atualmente visível no DOM
let formForVideo = currentForm;
if (!formForVideo) {
// Lê formsData se ainda não foi carregado
if (Object.keys(formsData).length === 0) {
try {
formsData = JSON.parse(formsJSON);
} catch (e) {
formsData = {};
}
}
formForVideo = detectCurrentForm();
// Se ainda não conseguiu detectar, usa a primeira como fallback
if (!formForVideo && formNames.length > 0) {
formForVideo = formNames[0];
}
}
const transitionVideo = formForVideo
? videos[formForVideo] || ""
: "";
if (transitionVideo && transitionVideo.trim() !== "") {
// Atualiza temporariamente o data-video-file (o sistema usa isso)
el.dataset.videoFile = transitionVideo;
}
}
}
} catch (e) {
console.error("[Forms] Erro ao processar vídeo de transição:", e);
}
// Usa o sistema normal de vídeo (mesmo que skills normais)
showVideoForIcon(el);
switchForm();
return;
}
if (isBack && !isFormSwitch && barStack.length) {
const prev = barStack.pop();
// Restaura currentForm se estava salvo no snapshot
if (prev.currentForm !== undefined) {
currentForm = prev.currentForm;
}
renderBarFromItems(prev);
const btn = document.querySelector(".skills-back-wrapper");
if (btn) btn.style.display = barStack.length ? "block" : "none";
return;
}
if (openSubs && subsRaw && subsRaw.trim() !== "") {
if (barStack.length && barStack[barStack.length - 1].parentIcon === el)
return;
try {
const subs = JSON.parse(subsRaw);
pushSubBarFrom(subs, el);
} catch { }
}
}
function wireClicksForCurrentBar() {
const currIcons = Array.from(iconsBar.querySelectorAll(".skill-icon"));
currIcons.forEach((el) => {
if (
el.dataset.weaponToggle === "1" ||
el.classList.contains("weapon-bar-toggle")
)
return;
if (el.dataset.wired) return;
el.dataset.wired = "1";
const label = el.dataset.nome || el.dataset.name || "";
el.setAttribute("aria-label", label);
if (el.hasAttribute("title")) el.removeAttribute("title");
const img = el.querySelector("img");
if (img) {
img.setAttribute("alt", "");
if (img.hasAttribute("title")) img.removeAttribute("title");
}
el.addEventListener("click", () => {
activateSkill(el, {
openSubs: true,
});
});
});
wireTooltipsForNewIcons();
applyEffectClasses();
}
function animateIconsBarEntrance() {
Array.from(iconsBar.children).forEach((c, i) => {
c.style.opacity = "0";
c.style.transform = "translateY(6px)";
requestAnimationFrame(() => {
setTimeout(() => {
c.style.transition = "opacity .18s ease, transform .18s ease";
c.style.opacity = "1";
c.style.transform = "translateY(0)";
}, i * 24);
});
});
}
function snapshotCurrentBarItemsFromDOM() {
const items = Array.from(iconsBar.querySelectorAll(".skill-icon"))
.filter((el) => el.dataset.weaponToggle !== "1")
.map((el) => {
const img = el.querySelector("img");
const iconURL = img ? img.src : "";
const subsRaw = el.dataset.subs || el.getAttribute("data-subs") || "";
let subs = null;
try {
subs = subsRaw ? JSON.parse(subsRaw) : null;
} catch {
subs = null;
}
const subattrsRaw = el.dataset.subattrs || "";
let flags = null;
if (el.dataset.flags) {
try {
flags = JSON.parse(el.dataset.flags);
} catch (e) { }
}
let weapon = null;
if (el.dataset.weapon) {
try {
weapon = JSON.parse(el.dataset.weapon);
} catch (e) { }
}
let effect = null;
if (el.dataset.effect) {
try {
effect = JSON.parse(el.dataset.effect);
} catch (e) { }
}
// Preserva data-form-videos para poder restaurar depois
const formVideos =
el.dataset.formVideos || el.getAttribute("data-form-videos") || "";
return {
name: el.dataset.nome || el.dataset.name || "",
index: el.dataset.index || "",
level: el.dataset.level || "",
desc: el.dataset.desc || "",
descPt: el.dataset.descPt || "",
descEn: el.dataset.descEn || "",
descEs: el.dataset.descEs || "",
descPl: el.dataset.descPl || "",
attrs: el.dataset.atr || el.dataset.attrs || "",
video: el.dataset.video || "",
iconURL,
subs,
subattrsStr: subattrsRaw,
flags: flags,
weapon: weapon,
effect: effect,
nested: el.dataset.nested || "",
subName: el.dataset.subName || "",
parentIndex: el.dataset.parentIndex || "",
formSwitch:
el.dataset.formSwitch ||
el.getAttribute("data-form-switch") ||
"",
formVideos: formVideos,
};
});
// Retorna objeto com items e currentForm para poder restaurar depois
return { items, currentForm: currentForm };
}
function ensureBackButton() {
const rail = iconsBar.closest(".top-rail.skills");
if (!rail) return null;
let wrap = rail.parentElement;
if (
!wrap ||
!wrap.classList ||
!wrap.classList.contains("skills-rail-wrap")
) {
const parentNode = rail.parentNode;
const newWrap = document.createElement("div");
newWrap.className = "skills-rail-wrap";
parentNode.insertBefore(newWrap, rail);
newWrap.appendChild(rail);
wrap = newWrap;
}
let backWrap = wrap.querySelector(".skills-back-wrapper");
if (!backWrap) {
backWrap = document.createElement("div");
backWrap.className = "skills-back-wrapper";
const btnInner = document.createElement("button");
btnInner.className = "skills-back";
btnInner.type = "button";
btnInner.setAttribute("aria-label", "Voltar");
btnInner.innerHTML =
'<svg class="back-chevron" width="100%" height="100%" viewBox="0 0 36 32" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" preserveAspectRatio="xMidYMid meet"><path d="M10 2L4 16L10 30" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M20 2L14 16L20 30" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/><path d="M30 2L24 16L30 30" stroke="currentColor" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
backWrap.appendChild(btnInner);
wrap.insertBefore(backWrap, rail);
btnInner.addEventListener("click", () => {
if (!barStack.length) return;
const prev = barStack.pop();
renderBarFromItems(prev);
backWrap.style.display = barStack.length ? "block" : "none";
wrap.classList.toggle("has-sub-bar", barStack.length > 0);
if (!barStack.length) btnInner.classList.remove("peek");
});
}
backWrap.style.display = barStack.length ? "block" : "none";
wrap.classList.toggle("has-sub-bar", barStack.length > 0);
const btnInner = backWrap.querySelector(".skills-back");
// Calcula a posição do botão baseado na posição real do top-rail
function updateBackButtonPosition() {
if (!rail || !backWrap) return;
const railRect = rail.getBoundingClientRect();
const wrapRect = wrap.getBoundingClientRect();
// Calcula a posição relativa do rail dentro do wrap
const railLeft = railRect.left - wrapRect.left;
// Posiciona o wrapper na borda esquerda do rail
// O botão interno usa translateX(-97%) para ficar atrás da barra
backWrap.style.left = railLeft + "px";
}
// Atualiza a posição quando necessário
if (backWrap.style.display !== "none") {
// Usa requestAnimationFrame para garantir que o DOM foi atualizado
requestAnimationFrame(() => {
updateBackButtonPosition();
});
// Recalcula em resize e scroll
if (!backWrap.dataset.positionWired) {
backWrap.dataset.positionWired = "1";
const updateOnResize = () => {
if (backWrap.style.display !== "none") {
updateBackButtonPosition();
}
};
window.addEventListener("resize", updateOnResize);
const observer = new ResizeObserver(() => {
if (backWrap.style.display !== "none") {
updateBackButtonPosition();
}
});
if (rail) observer.observe(rail);
observer.observe(wrap);
}
}
return btnInner;
}
function renderBarFromItems(itemsOrSnapshot) {
// Suporta tanto o formato antigo (array) quanto o novo (objeto com items e currentForm)
let items, savedCurrentForm;
if (Array.isArray(itemsOrSnapshot)) {
items = itemsOrSnapshot;
savedCurrentForm = null;
} else if (itemsOrSnapshot && itemsOrSnapshot.items) {
items = itemsOrSnapshot.items;
savedCurrentForm = itemsOrSnapshot.currentForm;
} else {
items = [];
savedCurrentForm = null;
}
// Restaura currentForm se estava salvo
if (savedCurrentForm !== undefined && savedCurrentForm !== null) {
currentForm = savedCurrentForm;
}
const tip = document.querySelector(".skill-tooltip");
if (tip) {
tip.setAttribute("aria-hidden", "true");
tip.style.opacity = "0";
tip.style.left = "-9999px";
tip.style.top = "-9999px";
}
iconsBar.innerHTML = "";
items.forEach((it, idx) => {
const node = document.createElement("div");
node.className = "skill-icon";
node.dataset.nome = it.name || "";
if (it.index) node.dataset.index = it.index;
if (it.level) node.dataset.level = it.level;
if (it.desc) node.dataset.desc = it.desc;
if (it.descPt) node.dataset.descPt = it.descPt;
if (it.descEn) node.dataset.descEn = it.descEn;
if (it.descEs) node.dataset.descEs = it.descEs;
if (it.descPl) node.dataset.descPl = it.descPl;
if (it.attrs) node.dataset.atr = it.attrs;
if (it.video) node.dataset.video = it.video;
if (it.subs) node.dataset.subs = JSON.stringify(it.subs);
if (it.subattrsStr) node.dataset.subattrs = it.subattrsStr;
if (it.flags) node.dataset.flags = JSON.stringify(it.flags);
if (
it.weapon &&
typeof it.weapon === "object" &&
Object.keys(it.weapon).length > 0
) {
node.dataset.weapon = JSON.stringify(it.weapon);
}
if (it.effect && typeof it.effect === "object") {
try {
node.dataset.effect = JSON.stringify(it.effect);
} catch (e) { }
}
// Restaura informações de subskill importantes para busca de vídeos
if (it.nested) node.dataset.nested = it.nested;
if (it.subName) node.dataset.subName = it.subName;
if (it.parentIndex) node.dataset.parentIndex = it.parentIndex;
if (!it.index && !it.nested) node.dataset.nested = "1";
// Restaura formSwitch (Change Form)
if (it.formSwitch) {
node.dataset.formSwitch = it.formSwitch;
node.setAttribute("data-form-switch", it.formSwitch);
}
// Restaura formVideos (vídeos de transição de forma)
if (it.formVideos) {
node.dataset.formVideos = it.formVideos;
node.setAttribute("data-form-videos", it.formVideos);
}
const img = document.createElement("img");
img.alt = "";
img.src = it.iconURL || (it.icon ? filePathURL(it.icon) : "");
img.decoding = "async";
img.loading = "lazy";
img.width = 50;
img.height = 50;
node.appendChild(img);
iconsBar.appendChild(node);
});
animateIconsBarEntrance();
wireClicksForCurrentBar();
// Remove qualquer toggle antigo que possa aparecer
const oldToggle = iconsBar.querySelector(".weapon-bar-toggle");
if (oldToggle) oldToggle.remove();
// Reaplica classes de weapon após renderizar barra
reapplyWeaponClassesToBar();
const b = ensureBackButton();
if (b) b.classList.add("peek");
// Atualiza a posição do botão back após a barra ser renderizada
requestAnimationFrame(() => {
const backWrap = document.querySelector(".skills-back-wrapper");
if (backWrap && backWrap.style.display !== "none") {
const rail = iconsBar.closest(".top-rail.skills");
const wrap = rail ? rail.parentElement : null;
if (rail && wrap && wrap.classList.contains("skills-rail-wrap")) {
const railRect = rail.getBoundingClientRect();
const wrapRect = wrap.getBoundingClientRect();
const railLeft = railRect.left - wrapRect.left;
// Posiciona na borda esquerda do rail (botão fica atrás com translateX)
backWrap.style.left = railLeft + "px";
}
}
});
}
function pushSubBarFrom(subs, parentIconEl) {
const tip = document.querySelector(".skill-tooltip");
if (tip) {
tip.setAttribute("aria-hidden", "true");
tip.style.opacity = "0";
tip.style.left = "-9999px";
tip.style.top = "-9999px";
}
const parentNameSnapshot = parentIconEl
? parentIconEl.dataset.nome || parentIconEl.dataset.name || ""
: "";
// Para subskills, usa parentIndex; para skills principais, usa index
const parentIndexSnapshot = parentIconEl
? parentIconEl.dataset.nested === "1"
? parentIconEl.dataset.parentIndex || ""
: parentIconEl.dataset.index || ""
: "";
const snapshot = snapshotCurrentBarItemsFromDOM();
barStack.push({
items: snapshot.items,
currentForm: snapshot.currentForm,
parentIcon: parentIconEl,
parentName: parentNameSnapshot,
parentIndex: parentIndexSnapshot,
});
ensureBackButton();
const langKey = getLangKey();
let cacheKey = null;
if (parentIconEl) {
cacheKey = parentIconEl.dataset.subCacheKey || null;
if (!cacheKey) {
if (parentIconEl.dataset.index) {
cacheKey = `idx:${parentIconEl.dataset.index}`;
} else {
const slug = slugify(
parentIconEl.dataset.nome || parentIconEl.dataset.name || ""
);
if (slug) cacheKey = `slug:${slug}`;
}
if (cacheKey) parentIconEl.dataset.subCacheKey = cacheKey;
}
}
if (cacheKey) {
const cached = subBarTemplateCache.get(cacheKey);
if (cached && cached.lang === langKey) {
iconsBar.innerHTML = "";
const clone = cached.template.cloneNode(true);
iconsBar.appendChild(clone);
animateIconsBarEntrance();
wireClicksForCurrentBar();
// Remove qualquer toggle antigo que possa aparecer
const oldToggle2 = iconsBar.querySelector(".weapon-bar-toggle");
if (oldToggle2) oldToggle2.remove();
// Reaplica classes de weapon após renderizar do cache
reapplyWeaponClassesToBar();
const cachedBtn = ensureBackButton();
if (cachedBtn) cachedBtn.classList.add("peek");
return;
}
}
const skillsRoot = document.getElementById("skills");
const i18nMap = skillsRoot
? JSON.parse(skillsRoot.dataset.i18nAttrs || "{}")
: {};
const L = i18nMap[getLangKey()] ||
i18nMap.pt || {
cooldown: "Recarga",
energy_gain: "Ganho de energia",
energy_cost: "Custo de energia",
power: "Poder",
power_pvp: "Poder PvP",
level: "Nível",
};
const hydratedSubs = inheritSubskillTree(subs, mainSkillsMeta);
const items = (hydratedSubs || [])
.filter((s) => {
// Filtra só se não tem nada útil
const hasName = (s.name || s.n || "").trim() !== "";
const hasIcon = (s.icon || "").trim() !== "";
const hasRef = (s.refS || s.refM || "").toString().trim() !== "";
return hasName || hasIcon || hasRef;
})
.map((s) => {
const name = (s.name || s.n || "").trim();
const desc = chooseDescFrom(s).replace(/(.*?)/g, "$1");
const attrsHTML = renderSubAttributesFromObj(s, L);
return {
name,
level: (s.level || "").toString().trim(),
desc,
descPt: s.descPt || (s.desc_i18n && s.desc_i18n.pt) || "",
descEn: s.descEn || (s.desc_i18n && s.desc_i18n.en) || "",
descEs: s.descEs || (s.desc_i18n && s.desc_i18n.es) || "",
descPl: s.descPl || (s.desc_i18n && s.desc_i18n.pl) || "",
attrs: "",
icon: s.icon || "",
iconURL: s.icon ? filePathURL(s.icon) : "",
video: s.video ? filePathURL(s.video) : "",
subs: Array.isArray(s.subs) ? s.subs : null,
subattrs: s,
flags: Array.isArray(s.flags) ? s.flags : null,
back:
s.back === true ||
s.back === "true" ||
s.back === "yes" ||
s.back === "1"
? "true"
: null,
weapon: s.weapon || null,
};
});
const fragment = document.createDocumentFragment();
items.forEach((it, iIdx) => {
const node = document.createElement("div");
node.className = "skill-icon";
node.dataset.nested = "1";
node.dataset.nome = it.name || "";
node.dataset.parentIndex = parentIndexSnapshot;
// Constrói o caminho completo para sub-subskills
// IMPORTANTE: O cache NÃO inclui o nome da skill principal no caminho
// Formato do cache: "SubskillName" ou "SubskillName:SubSubskillName" (sem o nome da skill principal)
let fullPath = it.name || "";
if (parentIconEl) {
// Se o pai é uma subskill (tem nested), adiciona o nome do pai ao caminho
if (parentIconEl.dataset.nested === "1") {
// Se o pai tem subName, usa ele (já está no formato correto, sem nome da skill principal)
if (
parentIconEl.dataset.subName &&
parentIconEl.dataset.subName.trim() !== ""
) {
fullPath = `${parentIconEl.dataset.subName}:${it.name || ""}`;
} else {
// Se o pai não tem subName, é a primeira subskill, então usa apenas o nome do pai
const parentName = (
parentIconEl.dataset.nome ||
parentIconEl.dataset.name ||
""
).trim();
fullPath = `${parentName}:${it.name || ""}`;
}
}
// Se o pai é uma skill principal (não é nested), o caminho é apenas o nome da subskill atual
// (já está definido como it.name acima)
}
node.dataset.subName = fullPath;
const subSlug = slugify(it.name || "");
if (subSlug) node.dataset.slug = subSlug;
if (it.level) node.dataset.level = it.level;
if (it.desc) node.dataset.desc = it.desc;
if (it.descPt) node.dataset.descPt = it.descPt;
if (it.descEn) node.dataset.descEn = it.descEn;
if (it.descEs) node.dataset.descEs = it.descEs;
if (it.descPl) node.dataset.descPl = it.descPl;
if (it.video) node.dataset.video = it.video;
if (it.subs) node.dataset.subs = JSON.stringify(it.subs);
if (it.subattrs) node.dataset.subattrs = JSON.stringify(it.subattrs);
if (it.flags) node.dataset.flags = JSON.stringify(it.flags);
if (it.back) node.dataset.back = it.back;
if (
it.weapon &&
typeof it.weapon === "object" &&
Object.keys(it.weapon).length > 0
) {
try {
node.dataset.weapon = JSON.stringify(it.weapon);
} catch (e) {
console.error(
"[Skills] Erro ao serializar weapon de subskill",
it.name,
e
);
}
}
const img = document.createElement("img");
img.alt = "";
img.src = it.iconURL;
img.decoding = "async";
img.loading = "lazy";
img.width = 50;
img.height = 50;
node.appendChild(img);
fragment.appendChild(node);
});
const templateClone = fragment.cloneNode(true);
iconsBar.innerHTML = "";
iconsBar.appendChild(fragment);
animateIconsBarEntrance();
wireClicksForCurrentBar();
// Remove qualquer toggle antigo que possa aparecer
const oldToggle3 = iconsBar.querySelector(".weapon-bar-toggle");
if (oldToggle3) oldToggle3.remove();
// Reaplica classes de weapon após renderizar subskills
reapplyWeaponClassesToBar();
const b2 = ensureBackButton();
if (b2) b2.classList.add("peek");
// Atualiza a posição do botão back após subskills serem renderizadas
requestAnimationFrame(() => {
const backWrap = document.querySelector(".skills-back-wrapper");
if (backWrap && backWrap.style.display !== "none") {
const rail = iconsBar.closest(".top-rail.skills");
const wrap = rail ? rail.parentElement : null;
if (rail && wrap && wrap.classList.contains("skills-rail-wrap")) {
const railRect = rail.getBoundingClientRect();
const wrapRect = wrap.getBoundingClientRect();
const railLeft = railRect.left - wrapRect.left;
// Posiciona na borda esquerda do rail (botão fica atrás com translateX)
backWrap.style.left = railLeft + "px";
}
}
});
if (cacheKey) {
subBarTemplateCache.set(cacheKey, {
template: templateClone,
lang: langKey,
});
}
}
window.addEventListener("gla:langChanged", () => {
subBarTemplateCache.clear();
const skillsRoot = document.getElementById("skills");
const i18nMap = skillsRoot
? JSON.parse(skillsRoot.dataset.i18nAttrs || "{}")
: {};
const lang = getLangKey();
Array.from(iconsBar.querySelectorAll(".skill-icon")).forEach((icon) => {
const pack = {
pt: icon.dataset.descPt || "",
en: icon.dataset.descEn || "",
es: icon.dataset.descEs || "",
pl: icon.dataset.descPl || "",
};
const chosen = (
pack[lang] ||
pack.pt ||
pack.en ||
pack.es ||
pack.pl ||
icon.dataset.desc ||
""
).trim();
if (chosen) icon.dataset.desc = chosen;
});
barStack.forEach((frame) => {
(frame.items || []).forEach((it) => {
const pack = {
pt: it.descPt,
en: it.descEn,
es: it.descEs,
pl: it.descPl,
};
const chosen =
pack[lang] ||
pack.pt ||
pack.en ||
pack.es ||
pack.pl ||
it.desc ||
"";
it.desc = chosen;
});
});
if (descBox) {
applyFlagTooltips(descBox);
}
const activeIcon = window.__lastActiveSkillIcon;
if (activeIcon && activeIcon.dataset.weapon) {
activateSkill(activeIcon, {
openSubs: false,
});
}
});
wireClicksForCurrentBar();
// Inicializa o sistema de swap de personagens (genérico)
// Aguarda um pouco para garantir que todos os ícones foram renderizados
setTimeout(() => {
initializeActiveCharacter();
}, 100);
const b0 = ensureBackButton();
if (b0) {
b0.classList.add("peek");
b0.style.alignSelf = "stretch";
}
// Move inicialização de tooltip para requestIdleCallback (não crítico)
if ("requestIdleCallback" in window) {
requestIdleCallback(
() => {
(function initSkillTooltip() {
if (document.querySelector(".skill-tooltip")) return;
const tip = document.createElement("div");
tip.className = "skill-tooltip";
tip.setAttribute("role", "tooltip");
tip.setAttribute("aria-hidden", "true");
document.body.appendChild(tip);
const lockUntilRef = {
value: 0,
};
function measureAndPos(el) {
if (!el || tip.getAttribute("aria-hidden") === "true") return;
tip.style.left = "0px";
tip.style.top = "0px";
const rect = el.getBoundingClientRect();
const tr = tip.getBoundingClientRect();
let left = Math.round(rect.left + (rect.width - tr.width) / 2);
left = Math.max(
8,
Math.min(left, window.innerWidth - tr.width - 8)
);
const coarse =
(window.matchMedia &&
matchMedia("(pointer: coarse)").matches) ||
window.innerWidth <= 600;
let top = coarse
? Math.round(rect.bottom + 10)
: Math.round(rect.top - tr.height - 8);
if (top < 8) top = Math.round(rect.bottom + 10);
tip.style.left = left + "px";
tip.style.top = top + "px";
}
function show(el, text) {
tip.textContent = text || "";
tip.setAttribute("aria-hidden", "false");
measureAndPos(el);
tip.style.opacity = "1";
}
function hide() {
tip.setAttribute("aria-hidden", "true");
tip.style.opacity = "0";
tip.style.left = "-9999px";
tip.style.top = "-9999px";
}
window.__globalSkillTooltip = {
show,
hide,
measureAndPos,
lockUntil: lockUntilRef,
};
Array.from(
document.querySelectorAll(".icon-bar .skill-icon")
).forEach((icon) => {
if (
icon.dataset.weaponToggle === "1" ||
icon.classList.contains("weapon-bar-toggle")
)
return;
if (icon.dataset.tipwired) return;
icon.dataset.tipwired = "1";
const label =
icon.dataset.nome || icon.dataset.name || icon.title || "";
if (label && !icon.hasAttribute("aria-label"))
icon.setAttribute("aria-label", label);
if (icon.hasAttribute("title")) icon.removeAttribute("title");
const img = icon.querySelector("img");
if (img) {
const imgAlt = img.getAttribute("alt") || "";
const imgTitle = img.getAttribute("title") || "";
if (!label && (imgAlt || imgTitle))
icon.setAttribute("aria-label", imgAlt || imgTitle);
img.setAttribute("alt", "");
if (img.hasAttribute("title")) img.removeAttribute("title");
}
icon.addEventListener("mouseenter", () => show(icon, label));
icon.addEventListener("mousemove", () => {
if (performance.now() >= lockUntilRef.value)
measureAndPos(icon);
});
icon.addEventListener("click", () => {
lockUntilRef.value = performance.now() + 240;
measureAndPos(icon);
});
icon.addEventListener("mouseleave", hide);
});
Array.from(
document.querySelectorAll(".subskills-rail .subicon")
).forEach((sub) => {
if (sub.dataset.tipwired) return;
sub.dataset.tipwired = "1";
const label =
sub.getAttribute("title") ||
sub.getAttribute("aria-label") ||
"";
if (label && !sub.hasAttribute("aria-label"))
sub.setAttribute("aria-label", label);
if (sub.hasAttribute("title")) sub.removeAttribute("title");
sub.addEventListener("mouseenter", () => show(sub, label));
sub.addEventListener("mousemove", () => {
if (performance.now() >= lockUntilRef.value) measureAndPos(sub);
});
sub.addEventListener("click", () => {
lockUntilRef.value = performance.now() + 240;
measureAndPos(sub);
});
sub.addEventListener("mouseleave", hide);
});
window.addEventListener(
"scroll",
() => {
const visible = document.querySelector(
'.skill-tooltip[aria-hidden="false"]'
);
if (!visible) return;
const target =
document.querySelector(".subskills-rail .subicon:hover") ||
document.querySelector(".subskills-rail .subicon.active") ||
document.querySelector(".icon-bar .skill-icon:hover") ||
document.querySelector(".icon-bar .skill-icon.active");
measureAndPos(target);
},
true
);
window.addEventListener("resize", () => {
const target =
document.querySelector(".subskills-rail .subicon:hover") ||
document.querySelector(".subskills-rail .subicon.active") ||
document.querySelector(".icon-bar .skill-icon:hover") ||
document.querySelector(".icon-bar .skill-icon.active");
measureAndPos(target);
});
})();
},
{ timeout: 2000 }
);
} else {
// Fallback para navegadores sem requestIdleCallback
setTimeout(() => {
(function initSkillTooltip() {
if (document.querySelector(".skill-tooltip")) return;
const tip = document.createElement("div");
tip.className = "skill-tooltip";
tip.setAttribute("role", "tooltip");
tip.setAttribute("aria-hidden", "true");
document.body.appendChild(tip);
window.__globalSkillTooltip = {
show: (el, text) => {
if (!el || !text) return;
tip.textContent = text;
tip.setAttribute("aria-hidden", "false");
measureAndPos(el);
},
hide: () => {
tip.setAttribute("aria-hidden", "true");
tip.style.left = "-9999px";
tip.style.top = "-9999px";
},
measureAndPos: (el) => {
if (!el || tip.getAttribute("aria-hidden") === "true") return;
const rect = el.getBoundingClientRect();
const tipRect = tip.getBoundingClientRect();
let left = rect.left + rect.width / 2 - tipRect.width / 2;
let top = rect.top - tipRect.height - 8;
if (left < 8) left = 8;
if (left + tipRect.width > window.innerWidth - 8)
left = window.innerWidth - tipRect.width - 8;
if (top < 8) top = rect.bottom + 8;
tip.style.left = left + "px";
tip.style.top = top + "px";
},
lockUntil: { value: 0 },
};
const { measureAndPos } = window.__globalSkillTooltip;
window.addEventListener(
"scroll",
() => {
const visible = document.querySelector(
'.skill-tooltip[aria-hidden="false"]'
);
if (!visible) return;
const target =
document.querySelector(".subskills-rail .subicon:hover") ||
document.querySelector(".subskills-rail .subicon.active") ||
document.querySelector(".icon-bar .skill-icon:hover") ||
document.querySelector(".icon-bar .skill-icon.active");
measureAndPos(target);
},
true
);
window.addEventListener("resize", () => {
const target =
document.querySelector(".subskills-rail .subicon:hover") ||
document.querySelector(".subskills-rail .subicon.active") ||
document.querySelector(".icon-bar .skill-icon:hover") ||
document.querySelector(".icon-bar .skill-icon.active");
measureAndPos(target);
});
})();
}, 100);
}
(function initTabs() {
const tabs = Array.from(document.querySelectorAll(".tab-btn"));
if (!tabs.length) return;
const contents = Array.from(document.querySelectorAll(".tab-content"));
const characterBox = document.querySelector(".character-box");
let wrapper = characterBox.querySelector(".tabs-height-wrapper");
if (!wrapper) {
wrapper = document.createElement("div");
wrapper.className = "tabs-height-wrapper";
contents.forEach((c) => {
wrapper.appendChild(c);
});
const tabsElement = characterBox.querySelector(".character-tabs");
if (tabsElement && tabsElement.nextSibling) {
characterBox.insertBefore(wrapper, tabsElement.nextSibling);
} else {
characterBox.appendChild(wrapper);
}
}
async function smoothHeightTransition(fromTab, toTab) {
if (!wrapper) return Promise.resolve();
const scrollY = window.scrollY;
const currentHeight = wrapper.getBoundingClientRect().height;
await new Promise((resolve) => {
const videoContainers = toTab.querySelectorAll(".video-container");
const contentCard = toTab.querySelector(".content-card");
if (videoContainers.length === 0) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => resolve());
});
});
return;
}
let lastHeight = 0;
let stableCount = 0;
const checksNeeded = 3;
let totalChecks = 0;
const maxChecks = 15;
function checkStability() {
totalChecks++;
const currentTabHeight = toTab.scrollHeight;
if (Math.abs(currentTabHeight - lastHeight) < 5) {
stableCount++;
} else {
stableCount = 0;
}
lastHeight = currentTabHeight;
if (stableCount >= checksNeeded || totalChecks >= maxChecks) {
resolve();
} else {
setTimeout(checkStability, 50);
}
}
setTimeout(checkStability, 50);
});
const nextHeight = toTab.getBoundingClientRect().height;
const finalHeight = Math.max(nextHeight, 100);
if (Math.abs(finalHeight - currentHeight) < 30) {
wrapper.style.height = "";
return Promise.resolve();
}
wrapper.style.overflow = "hidden";
wrapper.style.height = currentHeight + "px";
wrapper.offsetHeight;
wrapper.style.transition = "height 0.3s cubic-bezier(0.4, 0, 0.2, 1)";
requestAnimationFrame(() => {
wrapper.style.height = finalHeight + "px";
});
return new Promise((resolve) => {
setTimeout(() => {
wrapper.style.height = "";
wrapper.style.transition = "";
wrapper.style.overflow = "";
resolve();
}, 320);
});
}
tabs.forEach((btn) => {
if (btn.dataset.wiredTab) return;
btn.dataset.wiredTab = "1";
btn.addEventListener("click", () => {
const target = btn.getAttribute("data-tab");
const currentActive = contents.find((c) =>
c.classList.contains("active")
);
const nextActive = contents.find((c) => c.id === target);
if (currentActive === nextActive) return;
document.body.classList.add("transitioning-tabs");
if (currentActive) {
currentActive.style.opacity = "0";
currentActive.style.transform = "translateY(-8px)";
}
setTimeout(async () => {
contents.forEach((c) => {
if (c !== nextActive) {
c.style.display = "none";
c.classList.remove("active");
}
});
tabs.forEach((b) => b.classList.toggle("active", b === btn));
if (nextActive) {
nextActive.classList.add("active");
nextActive.style.display = "block";
nextActive.style.opacity = "0";
nextActive.style.visibility = "hidden";
nextActive.offsetHeight;
try {
if (target === "skills") {
const tabEl = document.getElementById(target);
if (tabEl) {
const activeIcon = tabEl.querySelector(
".icon-bar .skill-icon.active"
);
const firstIcon = tabEl.querySelector(
".icon-bar .skill-icon"
);
const toClick = activeIcon || firstIcon;
if (toClick) {
const had = document.body.dataset.suppressSkillPlay;
document.body.dataset.suppressSkillPlay = "1";
toClick.click();
if (had) document.body.dataset.suppressSkillPlay = had;
}
}
}
} catch (e) { }
}
if (currentActive && nextActive) {
await smoothHeightTransition(currentActive, nextActive);
}
if (nextActive) {
nextActive.style.visibility = "";
nextActive.style.transform = "translateY(12px)";
requestAnimationFrame(() => {
nextActive.style.opacity = "1";
nextActive.style.transform = "translateY(0)";
setTimeout(() => {
nextActive.style.opacity = "";
nextActive.style.transform = "";
document.body.classList.remove("transitioning-tabs");
try {
delete document.body.dataset.suppressSkillPlay;
} catch { }
}, 300);
});
}
}, 120);
setTimeout(() => {
syncDescHeight();
if (target === "skins") {
videosCache.forEach((v) => {
try {
v.pause();
} catch (e) { }
v.style.display = "none";
});
if (videoBox) {
videoBox.querySelectorAll("video.skill-video").forEach((v) => {
try {
v.pause();
} catch (e) { }
v.style.display = "none";
});
}
if (window.__subskills) window.__subskills.hideAll?.(videoBox);
const placeholder = videoBox?.querySelector(".video-placeholder");
if (videoBox && placeholder) {
placeholder.style.display = "none";
placeholder.classList.add("fade-out");
}
} else {
const activeIcon = document.querySelector(
".icon-bar .skill-icon.active"
);
if (activeIcon) activeIcon.click();
}
}, 450);
});
});
})();
(function initSkinsArrows() {
const carousel = $(".skins-carousel");
const wrapper = $(".skins-carousel-wrapper");
const left = $(".skins-arrow.left");
const right = $(".skins-arrow.right");
if (!carousel || !left || !right || !wrapper) return;
if (wrapper.dataset.wired) return;
wrapper.dataset.wired = "1";
const scrollAmt = () => Math.round(carousel.clientWidth * 0.6);
function setState() {
const max = carousel.scrollWidth - carousel.clientWidth;
const x = carousel.scrollLeft;
const hasLeft = x > 5,
hasRight = x < max - 5;
left.style.display = hasLeft ? "inline-block" : "none";
right.style.display = hasRight ? "inline-block" : "none";
wrapper.classList.toggle("has-left", hasLeft);
wrapper.classList.toggle("has-right", hasRight);
carousel.style.justifyContent = !hasLeft && !hasRight ? "center" : "";
}
function go(dir) {
const max = carousel.scrollWidth - carousel.clientWidth;
const next =
dir < 0
? Math.max(0, carousel.scrollLeft - scrollAmt())
: Math.min(max, carousel.scrollLeft + scrollAmt());
carousel.scrollTo({
left: next,
behavior: "smooth",
});
}
left.addEventListener("click", () => go(-1));
right.addEventListener("click", () => go(1));
carousel.addEventListener("scroll", setState);
new ResizeObserver(setState).observe(carousel);
setState();
})();
function renderAttributes(str) {
const skillsRoot = document.getElementById("skills");
const i18nMap = skillsRoot
? JSON.parse(skillsRoot.dataset.i18nAttrs || "{}")
: {};
const langRaw = (
document.documentElement.lang ||
skillsRoot?.dataset.i18nDefault ||
"pt"
).toLowerCase();
const langKey = i18nMap[langRaw]
? langRaw
: i18nMap[langRaw.split("-")[0]]
? langRaw.split("-")[0]
: "pt";
const L = i18nMap[langKey] ||
i18nMap.pt || {
cooldown: "Recarga",
energy_gain: "Ganho de energia",
energy_cost: "Custo de energia",
power: "Poder",
power_pvp: "Poder PvP",
level: "Nível",
};
const vals = (str || "").split(",").map((v) => v.trim());
// Processa valores, tratando strings vazias e "-" como NaN
// IMPORTANTE: ordem fixa é [powerpve, powerpvp, energy, cooldown]
const parseValue = (val) => {
if (!val || val === "" || val === "-") return NaN;
const parsed = parseFloat(val);
return isNaN(parsed) ? NaN : parsed;
};
const pve = parseValue(vals[0]);
const pvp = parseValue(vals[1]);
const ene = parseValue(vals[2]);
const cd = parseValue(vals[3]);
// Debug: log se houver problema na ordem
if (str && str.includes(",") && !isNaN(cd) && !isNaN(pvp) && cd === pvp) {
console.warn(
"[Skills] renderAttributes: possível problema na ordem dos atributos",
{ str, vals, pve, pvp, ene, cd }
);
}
const rows = [];
// Ordem de exibição: cooldown, energy, power, power_pvp
if (!isNaN(cd)) rows.push([L.cooldown, cd]);
if (!isNaN(ene) && ene !== 0) {
const label = ene > 0 ? L.energy_gain : L.energy_cost;
rows.push([label, Math.abs(ene)]);
}
if (!isNaN(pve)) rows.push([L.power, pve]);
if (!isNaN(pvp)) rows.push([L.power_pvp, pvp]);
// Debug: log se houver valores suspeitos (desabilitado para performance)
// if (str && str.includes(',')) {
// console.log('[Skills] renderAttributes processed', {
// str,
// vals: vals.slice(0, 4),
// parsed: { pve, pvp, ene, cd },
// rows: rows.map(r => r[0])
// });
// }
if (!rows.length) return "";
const html = rows
.map(
([rowLabel, rowValue]) =>
`
${rowLabel}${rowValue}
`
)
.join("");
return `
${html}
`;
}
function syncDescHeight() { }
window.addEventListener("resize", syncDescHeight);
if (videoBox) new ResizeObserver(syncDescHeight).observe(videoBox);
iconItems.forEach((el) => {
const wired = !!el.dataset._sync_wired;
if (wired) return;
el.dataset._sync_wired = "1";
el.addEventListener("click", () => {
Promise.resolve().then(syncDescHeight);
});
});
if (iconsBar) {
const scrollWrapper =
iconsBar.closest(".icon-scroll-x") || iconsBar.parentElement;
const scrollContainer =
scrollWrapper && scrollWrapper.classList.contains("icon-scroll-x")
? scrollWrapper
: iconsBar;
addOnce(
scrollContainer,
"wheel",
(e) => {
if (e.deltaY) {
e.preventDefault();
scrollContainer.scrollLeft += e.deltaY;
}
},
{ passive: false }
); // passive: false necessário porque usamos preventDefault()
}
wireClicksForCurrentBar();
if (iconItems.length) {
const first = iconItems[0];
if (first) {
activateSkill(first, {
openSubs: false,
});
}
}
// Aplica lazy loading em imagens fora do viewport inicial
(function applyLazyLoading() {
if ("IntersectionObserver" in window) {
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
if (img.tagName === "IMG" && !img.hasAttribute("loading")) {
img.loading = "lazy";
}
observer.unobserve(img);
}
});
});
// Observa imagens que não estão no viewport inicial
document
.querySelectorAll("img:not(.topbar-icon):not([loading])")
.forEach((img) => {
const rect = img.getBoundingClientRect();
if (rect.bottom > window.innerHeight || rect.top < 0) {
imageObserver.observe(img);
}
});
}
})();
setTimeout(() => {
Array.from(document.querySelectorAll(".skill-icon")).forEach((el) => { });
videosCache.forEach((v, idx) => {
const src = v.querySelector("source")
? v.querySelector("source").src
: v.src;
v.addEventListener("error", (ev) => { });
v.addEventListener("loadedmetadata", () => { });
});
}, 600);
})();
</script>