// ==UserScript== // @name POE2 WeGameBD增强 // @namespace local.codex.wegame.poe2 // @version 1.1.11 // @updated 2026-07-07 13:59:33 // @description 在 WeGame 流放之路2 BD 分享页底部展示可复制的文字版技能信息 // @author 维克牛 // @license MIT // @updateURL https://gitee.com/aprilfool/Script/raw/main/Tampermonkey/POE2-WeGameBD-enhancer/POE2-WeGameBD-enhancer.user.js // @downloadURL https://gitee.com/aprilfool/Script/raw/main/Tampermonkey/POE2-WeGameBD-enhancer/POE2-WeGameBD-enhancer.user.js // @match https://www.wegame.com.cn/helper/poe2/* // @run-at document-idle // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest // @connect poe2db.tw // @connect cdn.poe2db.tw // ==/UserScript== (function () { "use strict"; const API_BASE = "https://www.wegame.com.cn/api/v1/wegame.pallas.poe2.Profile"; const PANEL_ID = "codex-poe2-skill-text-panel"; const STYLE_ID = "codex-poe2-skill-text-style"; const SCRIPT_UPDATED_AT = "2026-07-07 13:59:33"; const COLLAPSE_STORAGE_KEY = "codex-poe2-wegamebd-collapse-v1"; const NAME_LANGS = ["cn", "tw", "us"]; const NAME_LANG_LABELS = { cn: "简体", tw: "繁体", us: "EN" }; let lastShareCode = ""; let lastText = ""; let lastRows = []; let lastEquipment = []; let lastJewels = []; let lastTalent = null; let lastBuildInfo = null; let mountWatcherStarted = false; let currentFilter = "all"; let currentSort = "original"; let currentNameLang = "cn"; let poe2dbNameMapsPromise = null; let poe2dbNameMaps = null; let nameLangState = "ready"; let nameLangMessage = ""; let panelEventGuardStarted = false; let lastPanelScrollSnapshot = null; let collapsedSections = readCollapsedSections(); function cleanText(value) { return String(value ?? "") .replace(/\[([^|\]]+)\|([^\]]+)\]/g, "$2") .replace(/\s+/g, " ") .trim(); } function escapeHtml(value) { return String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'", })[char]); } function readCollapsedSections() { try { const raw = localStorage.getItem(COLLAPSE_STORAGE_KEY); const parsed = raw ? JSON.parse(raw) : {}; return parsed && typeof parsed === "object" ? parsed : {}; } catch { return {}; } } function writeCollapsedSections() { try { localStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify(collapsedSections)); } catch { // localStorage may be blocked by browser privacy settings. } } function isSectionCollapsed(key) { return Boolean(key && collapsedSections[key]); } function setSectionCollapsed(key, collapsed) { if (!key) return; collapsedSections = { ...collapsedSections, [key]: Boolean(collapsed) }; writeCollapsedSections(); } function applySectionCollapsed(section, key) { const collapsed = isSectionCollapsed(key); section.classList.toggle("codex-collapsed", collapsed); const button = section.querySelector("[data-collapse-section]"); if (button) button.textContent = collapsed ? "展开" : "折叠"; } function sectionTitleHtml(title, detail, collapseKey = "") { const count = detail ? `${escapeHtml(detail)}` : ""; const action = collapseKey ? `` : ""; return `
${escapeHtml(title)} ${count} ${action}
`; } function normalizeNameKey(value) { return cleanText(value) .replace(/\s+/g, " ") .replace(/[()]/g, (char) => (char === "(" ? "(" : ")")) .trim() .toLowerCase(); } function cleanPoe2dbLabel(value) { return cleanText(value) .replace(/<[^>]+>/g, "") .replace(/\s+/g, " ") .trim(); } function simplifiedToTraditionalFallback(value) { const map = { "战":"戰","飞":"飛","级":"級","灵":"靈","体":"體","击":"擊","发":"發","触":"觸","电":"電","冰":"冰","霜":"霜","净":"淨","化":"化","虚":"虛","空":"空","冲":"衝","击":"擊","践":"踐","踏":"踏","守":"守","护":"護","野":"野","性":"性","报":"報","复":"復","从":"從","军":"軍","扩":"擴","范":"範","围":"圍","过":"過","载":"載","以":"以","己":"己","度":"度","人":"人","罗":"羅","米":"米","拉":"拉","阿":"阿","曼":"曼","娜":"娜","奉":"奉","纳":"納","持":"持","久":"久","地":"地","面":"面","狙":"狙","印":"印","记":"記","正":"正","义":"義","天":"天","降":"降","火":"火","焰":"焰","惧":"懼","赠":"贈","猎":"獵","鹿":"鹿","寒":"寒","捷":"捷","弹":"彈","幕":"幕","回":"迴","旋":"旋","斩":"斬","能":"能","量":"量","球":"球","增":"增","益":"益","效":"效","区":"區","域":"域","间":"間","间":"間","鱼":"魚","鸟":"鳥","龙":"龍","风":"風","云":"雲","会":"會","转":"轉","伤":"傷","害":"害","强":"強","袭":"襲","双":"雙","释":"釋","放":"放","压":"壓","制":"制","绝":"絕","对":"對","应":"應","导":"導","弹":"彈","连":"連","锁":"鎖","链":"鏈","击":"擊","扫":"掃","荡":"盪","圣":"聖","锤":"錘","锻":"鍛","炼":"煉","术":"術","师":"師","药":"藥","剂":"劑","药":"藥","药":"藥","药":"藥","药":"藥","药":"藥" }; return cleanText(value).replace(/[\u4e00-\u9fff]/g, (char) => map[char] || char); } function requestText(url) { const gmRequest = typeof GM_xmlhttpRequest === "function" ? GM_xmlhttpRequest : (typeof GM === "object" && typeof GM.xmlHttpRequest === "function" ? GM.xmlHttpRequest : null); if (gmRequest) { return new Promise((resolve, reject) => { gmRequest({ method: "GET", url, headers: { "User-Agent": "Mozilla/5.0", "Referer": "https://poe2db.tw/cn/", }, onload: (response) => { if (response.status >= 200 && response.status < 300) resolve(response.responseText); else reject(new Error(`${url} HTTP ${response.status}`)); }, onerror: () => reject(new Error(`请求失败:${url}`)), }); }); } return fetch(url, { credentials: "omit" }).then((response) => { if (!response.ok) throw new Error(`${url} HTTP ${response.status}`); return response.text(); }); } async function loadPoe2dbNameMaps(force = false) { if (!force && poe2dbNameMaps) return poe2dbNameMaps; if (!force && poe2dbNameMapsPromise) return poe2dbNameMapsPromise; if (force) poe2dbNameMapsPromise = null; poe2dbNameMapsPromise = (async () => { const html = await requestText("https://poe2db.tw/cn/"); const headerMatch = html.match(/https:\/\/cdn\.poe2db\.tw\/js\/poedb_header\.[a-f0-9]+\.js/); if (!headerMatch) throw new Error("未找到 POE2DB header 脚本"); const headerJs = await requestText(headerMatch[0]); const files = {}; for (const lang of NAME_LANGS) { const match = headerJs.match(new RegExp(`autocompletecb_${lang}\\.[a-z0-9]+\\.json`, "i")); if (match) files[lang] = `https://cdn.poe2db.tw/json/${match[0]}`; } const byValue = { ...(poe2dbNameMaps?.byValue || {}) }; const byCnName = new Map(poe2dbNameMaps?.byCnName || []); await Promise.allSettled(NAME_LANGS.map(async (lang) => { if (!files[lang]) return; try { const list = JSON.parse(await requestText(files[lang])); byValue[lang] = new Map(); for (const item of Array.isArray(list) ? list : []) { const value = cleanText(item.value || ""); const label = cleanPoe2dbLabel(item.label || ""); if (!value || !label) continue; byValue[lang].set(value, label); if (lang === "cn") { byCnName.set(label, value); byCnName.set(normalizeNameKey(label), value); } } } catch (error) { console.warn(`POE2DB ${lang} 名称数据加载失败`, error); } })); poe2dbNameMaps = { byValue, byCnName }; return poe2dbNameMaps; })(); return poe2dbNameMapsPromise; } function localizedName(name) { const cleanName = cleanText(name); if (!cleanName || currentNameLang === "cn" || !poe2dbNameMaps) return cleanName; const value = poe2dbNameMaps.byCnName.get(cleanName) || poe2dbNameMaps.byCnName.get(normalizeNameKey(cleanName)); if (!value) return currentNameLang === "tw" ? simplifiedToTraditionalFallback(cleanName) : cleanName; return poe2dbNameMaps.byValue[currentNameLang]?.get(value) || (currentNameLang === "tw" ? simplifiedToTraditionalFallback(cleanName) : cleanName); } async function ensureNameLanguageLoaded() { if (currentNameLang === "cn") { nameLangState = "ready"; nameLangMessage = ""; updateRenderedLanguage(); return true; } if (poe2dbNameMaps?.byValue?.[currentNameLang]) { nameLangState = "ready"; nameLangMessage = ""; updateRenderedLanguage(); return true; } nameLangState = "loading"; nameLangMessage = `${NAME_LANG_LABELS[currentNameLang]} 名称加载中`; updateRenderedLanguage(); try { await loadPoe2dbNameMaps(Boolean(poe2dbNameMaps)); nameLangState = "ready"; nameLangMessage = ""; updateRenderedLanguage(); return true; } catch (error) { nameLangState = "error"; nameLangMessage = "POE2DB 名称加载失败"; console.warn("POE2DB 技能名称加载失败", error); updateRenderedLanguage(); return false; } } function localizedSupportText(support) { const name = localizedName(support.name || "-"); const suffix = [ support.level && support.level !== "-" ? `Lv${support.level}` : "", support.quality && support.quality !== "-" ? support.quality : "", support.details?.length ? support.details.join(" / ") : "", ].filter(Boolean).join(" "); return suffix ? `${name}(${suffix})` : name; } function insertedRows(row) { if (Array.isArray(row?.inserted)) return row.inserted; return [ ...(row?.supports || []).map((support) => ({ ...support, rowType: support.rowType || "被动" })), ...(row?.nestedActives || []).map((active) => ({ ...active, rowType: active.rowType || "主动" })), ]; } function localizedRowText(row, index) { const lines = []; lines.push(`${index + 1}. ${localizedName(row.name)} | Lv${row.level} | ${row.quality} | ${row.sockets || "-"}孔`); lines.push(` 类型: ${row.skillType || "-"}`); if (row.tags) lines.push(` 标签: ${row.tags}`); if (row.requirements) lines.push(` 需求: ${row.requirements}`); if (row.details?.length) lines.push(` 属性: ${row.details.join(" / ")}`); const tableRows = insertedRows(row); lines.push(` 已插入技能: ${tableRows.length ? tableRows.map((item) => `${item.rowType}:${localizedSupportText(item)}`).join(" / ") : "-"}`); return lines.join("\n"); } function currentCopyText() { if (!lastBuildInfo && !lastTalent && !lastEquipment.length && !lastJewels.length && (!lastRows.length || currentNameLang === "cn")) return lastText; const parts = []; if (lastBuildInfo) parts.push("【BD信息】\n" + formatBuildInfoText(lastBuildInfo)); if (lastEquipment.length) parts.push("【装备/药剂/护符】\n" + lastEquipment.map(formatItemText).join("\n\n")); if (lastJewels.length) parts.push("【珠宝】\n" + lastJewels.map(formatItemText).join("\n\n")); if (lastTalent) parts.push("【天赋/任务奖励】\n" + formatTalentText(lastTalent)); if (lastRows.length) parts.push("【技能】\n" + lastRows.map((row, index) => localizedRowText(row, index)).join("\n\n")); return parts.filter(Boolean).join("\n\n"); } function extractShareCode() { const href = location.href; if (href.includes("#/share/")) { return href.split("#/share/")[1].split("?")[0].split("&")[0].replace(/^\/+|\/+$/g, ""); } const hash = location.hash || ""; if (hash.startsWith("#/share/")) { return hash.slice("#/share/".length).split("?")[0].replace(/^\/+|\/+$/g, ""); } return ""; } function roleBody(shareCode, role) { const r = role || {}; return { area: r.area || 0, openid: r.openid || "", role_id: String(r.role_id || ""), share_code: shareCode, from_src: "poe2_helper", }; } async function postJson(endpoint, body) { const response = await fetch(`${API_BASE}/${endpoint}`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!response.ok) { throw new Error(`${endpoint} HTTP ${response.status}`); } return response.json(); } function propValue(item, names) { const wanted = new Set(names); for (const prop of item?.properties || []) { if (!wanted.has(cleanText(prop.name))) continue; const first = prop.values?.[0]?.[0]; return cleanText(first); } return ""; } function fieldValueText(field) { if (typeof field === "string") return cleanText(field); const values = field?.values; if (Array.isArray(values)) { const parts = values .map((value) => Array.isArray(value) ? value[0] : value) .map((value) => cleanText(value)) .filter(Boolean); if (parts.length) return parts.join(" "); } return cleanText(field?.value ?? field?.displayValue ?? field?.text ?? ""); } function fieldText(field) { if (typeof field === "string") return cleanText(field); const name = cleanText(field?.name || field?.type || field?.id || ""); const value = fieldValueText(field); return cleanText(name && value ? `${name} ${value}` : (name || value)); } function isSpiritRequirementText(text) { return /(精魂|Spirit).{0,24}(消耗|保留|reservation|reserve|cost)|(?:消耗|保留|reservation|reserve|cost).{0,24}(精魂|Spirit)/i.test(text); } function isSkillDetailText(text) { if (isSpiritRequirementText(text)) return false; return /(消耗|Cost|保留|Reservation|Reserve|冷却|Cooldown|施放时间|Cast Time|攻击速度|Attack Time|攻击伤害|Attack Damage|暴击|Critical|加成|Multiplier|额外保留|Additional Reservation)/i.test(text); } function spiritRequirementParts(item) { const fields = [ ...(item?.properties || []), ...(item?.requirements || []), ...(item?.explicitMods || []), ...(item?.implicitMods || []), ]; return fields .map(fieldText) .filter((text) => text && isSpiritRequirementText(text)); } function skillDetailParts(item) { return (item?.properties || []) .map(fieldText) .filter((text) => text && isSkillDetailText(text)); } function formatRequirements(item) { const requirements = Array.isArray(item?.requirements) ? item.requirements : []; const parts = requirements.map((requirement) => { if (typeof requirement === "string") return cleanText(requirement); const name = cleanText(requirement?.name || requirement?.type || requirement?.id || ""); const value = fieldValueText(requirement); if (name && value) return `${name} ${value}`; return name || value; }).filter(Boolean); for (const spiritPart of spiritRequirementParts(item)) { if (!parts.includes(spiritPart)) parts.push(spiritPart); } return parts.join(" / "); } function gemName(item) { return cleanText(item?.typeLine || item?.baseType || item?.name || ""); } function formatQuality(value) { const text = cleanText(value).replace(/^Q/i, "").replace(/^\+/, "").replace(/%$/, "").trim(); if (!text || text === "-") return "品质0%"; return `品质${text}%`; } function formatLevel(value) { const text = cleanText(value); if (!text || text === "-") return "-"; if (text.includes("最高等级")) return text; const match = text.match(/\d+/); if (match && Number(match[0]) >= 20) return `${text}(最高等级)`; return text; } function isSupport(item) { return Boolean(item?.support) || item?.frameTypeId === "SupportGem"; } function isLineageSupport(item) { const fields = [ item?.typeLine, item?.baseType, item?.name, ...(item?.properties || []), ...(item?.requirements || []), ...(item?.explicitMods || []), ...(item?.implicitMods || []), ]; return fields.some((field) => /LineageSupports|Lineage|血脉|血脈|血统|血統/i.test(fieldText(field))); } function hasExplicitSpiritCost(skill) { const fields = [ ...(skill?.properties || []), ...(skill?.requirements || []), ...(skill?.explicitMods || []), ...(skill?.implicitMods || []), ]; return fields.some((field) => { const text = fieldText(field); return isSpiritRequirementText(text); }); } function skillType(skill, tags) { const inventoryId = cleanText(skill?.inventoryId || ""); if (inventoryId && !["SkillSlots", "AscendancySkills"].includes(inventoryId)) { return { label: "装备赋予", className: "codex-type-granted" }; } if (hasExplicitSpiritCost(skill)) { return { label: "精魂技能", className: "codex-type-spirit" }; } if (/\b(Persistent|Aura|Herald)\b|永久性|光环|捷/.test(tags)) { return { label: "被动技能", className: "codex-type-passive" }; } return { label: "主动技能", className: "codex-type-active" }; } function formatSkill(skill, index) { const name = gemName(skill) || `技能${index + 1}`; const level = propValue(skill, ["等级"]) || "-"; const quality = propValue(skill, ["品质"]) || "-"; const sockets = Array.isArray(skill.gemSockets) ? skill.gemSockets.length : ""; const tags = cleanText(skill.properties?.[0]?.name || ""); const requirements = formatRequirements(skill); const type = skillType(skill, tags); const supports = []; const nestedActives = []; const inserted = []; for (const child of skill.socketedItems || []) { const childName = gemName(child); if (!childName) continue; if (isSupport(child)) { const childLevel = propValue(child, ["等级"]); const childQuality = propValue(child, ["品质"]); const childDetails = skillDetailParts(child); const suffix = [ childLevel ? `Lv${formatLevel(childLevel)}` : "", childQuality && childQuality !== "-" ? formatQuality(childQuality) : "", childDetails.length ? childDetails.join(" / ") : "", ].filter(Boolean).join(" "); const supportRow = { name: childName, rowType: isLineageSupport(child) ? "血脉" : "被动", level: childLevel ? formatLevel(childLevel) : "-", quality: childQuality && childQuality !== "-" ? formatQuality(childQuality) : "-", details: childDetails, text: suffix ? `${childName}(${suffix})` : childName, }; supports.push(supportRow); inserted.push(supportRow); } else { const childLevel = propValue(child, ["等级"]); const childQuality = propValue(child, ["品质"]); const childSockets = Array.isArray(child.gemSockets) ? child.gemSockets.length : ""; const childRequirements = formatRequirements(child); const childDetails = skillDetailParts(child); const childRow = { name: childName, rowType: "主动", level: childLevel ? formatLevel(childLevel) : "-", quality: childQuality && childQuality !== "-" ? formatQuality(childQuality) : "品质0%", sockets: childSockets || "-", requirements: childRequirements || "-", details: childDetails, }; childRow.text = `${childName}${childRow.level !== "-" ? ` Lv${childRow.level}` : ""} ${childRow.quality}${childSockets ? ` ${childSockets}孔` : ""}${childDetails.length ? ` 属性:${childDetails.join(" / ")}` : ""}${childRow.requirements !== "-" ? ` 需求:${childRow.requirements}` : ""}`; nestedActives.push(childRow); inserted.push(childRow); } } const displayLevel = formatLevel(level); const lines = []; lines.push(`${index + 1}. ${name} | Lv${displayLevel} | ${formatQuality(quality)} | ${sockets || "-"}孔`); lines.push(` 类型: ${type.label}`); if (tags) lines.push(` 标签: ${tags}`); if (requirements) lines.push(` 需求: ${requirements}`); const details = skillDetailParts(skill); if (details.length) lines.push(` 属性: ${details.join(" / ")}`); const tableRows = inserted; lines.push(` 已插入技能: ${tableRows.length ? tableRows.map((item) => `${item.rowType}:${item.text || item.name}`).join(" / ") : "-"}`); return { name, originalIndex: index, level: displayLevel, quality: formatQuality(quality), sockets: sockets || "-", tags, requirements, details, skillType: type.label, skillTypeClass: type.className, supports, nestedActives, inserted, text: lines.join("\n"), }; } function formatSkills(payload, role) { const skills = payload?.skills || []; const roleTitle = [ cleanText(role?.name), role?.level ? `Lv${role.level}` : "", cleanText(role?.class_name || role?.class), ].filter(Boolean).join(" "); const lines = []; lines.push("WeGame POE2 BD 技能信息"); if (roleTitle) lines.push(`角色: ${roleTitle}`); lines.push(`链接: ${location.href}`); lines.push(`生成时间: ${new Date().toLocaleString()}`); lines.push(""); if (!skills.length) { lines.push("未读取到接口技能数据。"); return { text: lines.join("\n"), rows: [] }; } const rows = []; skills.forEach((skill, index) => { const row = formatSkill(skill, index); rows.push(row); lines.push(row.text); lines.push(""); }); return { text: lines.join("\n").trim(), rows }; } function itemData(entry) { return entry?.itemData || entry || {}; } function itemName(entry) { const item = itemData(entry); return cleanText([item.name, item.typeLine || item.baseType].filter(Boolean).join(" ")) || cleanText(item.display_name || item.typeLine || item.baseType || item.name || "-"); } function itemBaseType(entry) { const item = itemData(entry); return cleanText(item.typeLine || item.baseType || item.name || ""); } function itemRarity(entry) { const item = itemData(entry); const map = { 0: "普通", 1: "魔法", 2: "稀有", 3: "传奇", 4: "宝石", 5: "通货", 9: "遗物", Normal: "普通", Magic: "魔法", Rare: "稀有", Unique: "传奇", Gem: "宝石", Currency: "通货", Relic: "遗物", }; const raw = item.rarity || item.frameTypeId || item.frameType; return cleanText(map[raw] || raw || "-"); } function rarityClass(rarity) { const label = cleanText(rarity); if (/传奇|Unique/i.test(label)) return "codex-rarity-unique"; if (/稀有|Rare/i.test(label)) return "codex-rarity-rare"; if (/魔法|Magic/i.test(label)) return "codex-rarity-magic"; return "codex-rarity-normal"; } function itemCategory(entry) { const item = itemData(entry); const raw = cleanText(item.inventoryId || entry?.inventoryId || entry?.itemSlot || entry?.socket_name || ""); const label = cleanText([item.name, item.typeLine, item.baseType, item.display_name].filter(Boolean).join(" ")); const propText = (item.properties || []).map((field) => cleanText(field?.name || fieldText(field))).filter(Boolean).join(" "); if (/^Amulet$/i.test(raw)) return "项链"; if (/^Charm/i.test(raw) || (/^Flask$/i.test(raw) && /(^|\s)(护符|咒符)(\s|$)|\bCharm\b(?!\s*Slots)/i.test(propText))) return "护符"; if (/^LifeFlask$/i.test(raw) || /生命|Life/i.test([label, propText].join(" "))) return "生命药剂"; if (/^ManaFlask$/i.test(raw) || /魔力|Mana/i.test([label, propText].join(" "))) return "魔力药剂"; if (/Flask/i.test(raw) || /药剂|\bFlask\b/i.test([label, propText].join(" "))) return "药剂"; if (!/^Amulet$/i.test(raw) && /(^|\s)(护符|咒符)(\s|$)|\bCharm\b/i.test(label)) return "护符"; return ""; } function itemSlot(entry, index) { const item = itemData(entry); const raw = cleanText(item.inventoryId || entry?.inventoryId || entry?.itemSlot || entry?.socket_name || ""); const category = itemCategory(entry); const map = { BodyArmour: "胸甲", Helm: "头盔", Gloves: "手套", Boots: "鞋子", Belt: "腰带", Amulet: "项链", Ring: "戒指", Ring2: "戒指2", Ring3: "戒指3", Weapon: "武器", Weapon2: "武器2", Offhand: "副手", Offhand2: "副手2", LifeFlask: "生命药剂", ManaFlask: "魔力药剂", Flask: "药剂", Charm: "护符", Charms: "护符", PassiveJewels: "珠宝", }; if (/^(Flask|LifeFlask|ManaFlask|Charm|Charms)$/i.test(raw) && category) return category; if (map[raw]) return map[raw]; if (category) return category; return raw || `#${index + 1}`; } function itemKind(entry, fallbackKind) { const item = itemData(entry); const raw = cleanText(item.inventoryId || entry?.inventoryId || entry?.itemSlot || ""); const category = itemCategory(entry); if (category && !/^Amulet$/i.test(raw)) return category; return fallbackKind; } function itemProperties(entry) { const item = itemData(entry); return (item.properties || []) .map((field) => { const name = cleanText(field?.name || ""); const value = fieldValueText(field); return name && value ? `${name} ${value}` : name || value; }) .filter(Boolean) .filter((text) => !/Stack Size|堆叠数量|Limited to/i.test(text)) .join(" / "); } function cleanModText(value) { return cleanText(value).replace(/<[^>]+>/g, "").replace(/\{|\}/g, "").trim(); } function cleanQuestStatText(value) { const text = cleanModText(value); const brokenFaceMatch = text.match(/^(\d+)\s*个?\s*毁面首领$/); if (brokenFaceMatch) return `击杀${brokenFaceMatch[1]}个boss`; return text; } function itemMods(entry) { const item = itemData(entry); const groups = [ ["底材词缀", item.implicitMods], ["附魔/涂油", item.enchantMods], ["装备词缀", item.explicitMods], ["破裂", item.fracturedMods], ["工艺", item.craftedMods], ["符文/镶嵌", item.runeMods], ["特殊词缀", item.desecratedMods], ["绑定", item.bondedMods], ["授予技能", item.grantedSkills], ]; return groups.map(([label, mods]) => ({ label, lines: (mods || []).map((mod) => { if (typeof mod === "string") return cleanModText(mod); const name = cleanText(mod?.name || mod?.type || ""); const value = fieldValueText(mod); return cleanModText([name, value].filter(Boolean).join(" ")); }).filter(Boolean), })).filter((group) => group.lines.length); } function equipmentSortValue(row) { if (row.kind === "护符" || row.slot === "护符") return 92; if (row.kind === "生命药剂" || row.slot === "生命药剂") return 120; if (row.kind === "魔力药剂" || row.slot === "魔力药剂") return 121; if (row.kind === "药剂" || /药剂/.test(row.slot || "")) return 122; const order = { Weapon: 10, Offhand: 11, Weapon2: 20, Offhand2: 21, Helm: 30, BodyArmour: 40, Gloves: 50, Boots: 60, Amulet: 70, Ring: 80, Ring2: 81, Ring3: 82, Belt: 90, Charm: 92, Charms: 92, LifeFlask: 120, ManaFlask: 121, Flask: 122, }; if (order[row.inventoryId] != null) return order[row.inventoryId]; return order[row.inventoryId] ?? 999; } function socketedItemNames(entry) { const item = itemData(entry); return (item.socketedItems || []).map((socketed) => cleanText(socketed.typeLine || socketed.baseType || socketed.name || "")).filter(Boolean); } function normalizeEquipmentItem(entry, index, kind) { const item = itemData(entry); const properties = itemProperties(entry); const requirements = formatRequirements(item); return { originalIndex: index, kind, slot: itemSlot(entry, index), inventoryId: cleanText(item.inventoryId || entry?.inventoryId || ""), name: itemName(entry), baseType: itemBaseType(entry), rarity: itemRarity(entry), properties: [properties, requirements ? `需求 ${requirements}` : ""].filter(Boolean).join(" / ") || "-", mods: itemMods(entry), socketed: socketedItemNames(entry), corrupted: Boolean(item.corrupted), ilvl: item.ilvl || "", position: [item.x != null ? `x${item.x}` : "", item.y != null ? `y${item.y}` : ""].filter(Boolean).join(" / "), }; } function normalizeEquipments(payload) { return (payload?.equipments || []) .map((entry, index) => normalizeEquipmentItem(entry, index, itemKind(entry, "装备"))) .filter((item) => item.name && item.name !== "-") .sort((a, b) => equipmentSortValue(a) - equipmentSortValue(b) || a.originalIndex - b.originalIndex); } function parseJewelData(payload) { const raw = payload?.jewel_data; if (Array.isArray(raw)) return raw; if (typeof raw !== "string" || !raw.trim()) return []; try { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed : []; } catch (error) { console.warn("WeGame 珠宝数据解析失败", error); return []; } } function jewelRarity(value) { const map = { 0: "普通", 1: "魔法", 2: "稀有", 3: "传奇", Normal: "普通", Magic: "魔法", Rare: "稀有", Unique: "传奇" }; return map[value] || cleanText(value || "-"); } function jewelSlotName(entry, index) { const socketName = cleanText(entry?.socket_name || ""); const socketId = cleanText(entry?.socket_id || ""); if (socketName && !/^jewel_slot\d+$/i.test(socketName)) return socketName; return `珠宝槽 ${index + 1}`; } function normalizeJewels(payload) { return parseJewelData(payload).map((entry, index) => { const jewel = entry?.jewel || {}; const mods = (jewel.mod_descriptions || []).map((group) => ({ label: Number(group?.type) === 2 ? "特殊词缀" : "装备词缀", lines: (group?.values_formats || group?.values || []).map((value) => cleanModText(value?.des || value)).filter(Boolean), })).filter((group) => group.lines.length); return { originalIndex: index, kind: "珠宝", slot: jewelSlotName(entry, index), inventoryId: cleanText(entry?.socket_id || ""), name: cleanText([jewel.display_name, jewel.name].filter(Boolean).join(" ")) || "-", baseType: cleanText(jewel.name || ""), rarity: jewelRarity(jewel.rarity), properties: "-", mods, socketed: [], corrupted: Boolean(jewel.corrupted || entry?.corrupted), ilvl: "", position: jewelSlotName(entry, index), }; }).filter((item) => item.name && item.name !== "-"); } function splitListText(text) { const clean = cleanText(text); if (!clean || clean === "-") return []; return clean.split(/\s+\/\s+/).map((part) => cleanText(part)).filter(Boolean); } function itemStatusText(row) { const parts = []; if (row.corrupted) parts.push("已腐化"); if (row.ilvl) parts.push(`物品等级 ${row.ilvl}`); return parts.join(" / "); } function formatItemText(row, index) { const lines = []; const status = itemStatusText(row); lines.push(`${index + 1}. [${row.kind}] ${row.slot} | ${localizedName(row.name)} | ${row.rarity}${status ? ` | ${status}` : ""}`); const props = splitListText(row.properties); if (props.length) { lines.push(" 属性/需求:"); props.forEach((prop) => lines.push(` - ${prop}`)); } if (row.socketed.length) { lines.push(" 插槽:"); row.socketed.forEach((name) => lines.push(` - ${localizedName(name)}`)); } for (const group of row.mods) { lines.push(` ${group.label}:`); group.lines.forEach((mod, modIndex) => lines.push(` ${modIndex + 1}. ${mod}`)); } return lines.join("\n"); } function normalizeBuildInfo(summaryPayload, role) { const title = cleanText(summaryPayload?.summary_title || ""); const timeBd = cleanText(summaryPayload?.time_bd || ""); const roleTitle = [ cleanText(role?.name), role?.level ? `Lv${role.level}` : "", cleanText(role?.class_name || role?.class), ].filter(Boolean).join(" "); if (!title && !timeBd && !roleTitle) return null; return { title, timeBd, roleTitle }; } function formatBuildInfoText(info) { return [ info.roleTitle ? `角色: ${info.roleTitle}` : "", info.title ? `BD标题: ${info.title}` : "", info.timeBd ? `BD时间: ${info.timeBd}` : "", `链接: ${location.href}`, ].filter(Boolean).join("\n"); } function normalizeTalent(talentPayload, profilePayload) { const tree = talentPayload?.talent_tree || {}; const hashes = Array.isArray(tree.hashes) ? tree.hashes : []; const specialisations = tree.specialisations || {}; const set1 = Array.isArray(specialisations.set1) ? specialisations.set1 : []; const set2 = Array.isArray(specialisations.set2) ? specialisations.set2 : []; const questStats = Array.isArray(tree.quest_stats) ? tree.quest_stats.map(cleanQuestStatText).filter(Boolean) : []; const jewelData = tree.jewel_data && typeof tree.jewel_data === "object" ? Object.keys(tree.jewel_data) : []; const assigned = Number(profilePayload?.assigned_talent_count || 0) || hashes.length || 0; const total = Number(profilePayload?.total_talent_count || 0) || ""; if (!assigned && !total && !questStats.length && !set1.length && !set2.length && !jewelData.length) return null; return { assigned, total, set1Count: set1.length, set2Count: set2.length, jewelSocketCount: jewelData.length, questStats }; } function formatTalentText(talent) { const lines = []; if (talent.assigned) lines.push(`天赋点: ${talent.assigned}`); lines.push(`武器1: ${talent.set1Count || 0}`); lines.push(`武器2: ${talent.set2Count || 0}`); if (talent.jewelSocketCount) lines.push(`天赋珠宝槽: ${talent.jewelSocketCount}`); if (talent.questStats?.length) { lines.push("任务奖励:"); talent.questStats.forEach((stat, index) => lines.push(` ${index + 1}. ${stat}`)); } return lines.join("\n"); } function domFallbackText() { const pageText = document.body?.innerText || ""; const marker = pageText.indexOf("技能"); if (marker < 0) return ""; const text = pageText.slice(marker).trim(); const stopMarkers = ["天赋", "装备", "珠宝", "属性"]; let end = text.length; for (const markerText of stopMarkers) { const idx = text.indexOf(`\n${markerText}\n`, 10); if (idx > 0) end = Math.min(end, idx); } return text.slice(0, end).trim(); } function ensureStyle() { if (document.getElementById(STYLE_ID)) return; const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = ` #${PANEL_ID} { box-sizing: border-box; display: block !important; width: min(900px, 100%); max-width: 100%; height: auto !important; min-height: 0 !important; margin: 18px 0 0; padding: 0 !important; overflow: visible !important; overflow-anchor: none; background: transparent !important; color: #c8d2dc; font-family: "Microsoft YaHei UI", "Microsoft YaHei", sans-serif; -webkit-user-select: text !important; user-select: text !important; } html, body, .share-page, .bd-content, .bd-content-inner, .skill-panel { overflow-anchor: none !important; } #${PANEL_ID} *, #${PANEL_ID} .codex-skill-body, #${PANEL_ID} .codex-skill-card, #${PANEL_ID} .codex-skill-line, #${PANEL_ID} .codex-skill-name, #${PANEL_ID} .codex-skill-meta { -webkit-user-select: text !important; user-select: text !important; } #${PANEL_ID}.codex-body-mounted { width: min(820px, calc(100vw - 760px)); min-width: 640px; margin: 18px 0 56px max(260px, calc((100vw - 1280px) / 2)); } #${PANEL_ID}.codex-skill-panel-mounted { position: relative !important; z-index: 1; width: 986px; max-width: 100%; margin: 18px 0 0 96px; padding: 0 !important; color: #b9aa88; } #${PANEL_ID} .codex-skill-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 54px; padding: 0 24px; background: #202c37; color: #aebcca; border-radius: 2px 2px 0 0; } #${PANEL_ID} .codex-skill-title { font-size: 20px; font-weight: 400; letter-spacing: 0; } #${PANEL_ID} .codex-skill-updated { margin-left: 10px; color: rgba(169, 157, 125, 0.72); font-size: 12px; font-weight: 400; white-space: nowrap; } #${PANEL_ID} .codex-skill-actions { display: flex; gap: 8px; } #${PANEL_ID} button { -webkit-user-select: none !important; user-select: none !important; cursor: pointer; border: 1px solid rgba(159, 179, 198, 0.22); border-radius: 3px; background: #263543; color: #c8d2dc; padding: 5px 10px; font-size: 13px; } #${PANEL_ID} button:hover { background: #304252; } #${PANEL_ID} button.codex-active-control { border-color: currentColor; color: #f0d99a; background: rgba(150, 127, 82, 0.18); } #${PANEL_ID} button:disabled { cursor: default; opacity: 0.58; } #${PANEL_ID} .codex-skill-controls { display: grid; grid-template-columns: 1fr auto; align-items: start; gap: 8px 18px; padding: 10px 16px; border: 1px solid rgba(150, 127, 82, 0.35); border-top: 0; background: rgba(10, 10, 9, 0.84); } #${PANEL_ID} .codex-skill-controls.codex-global-controls { display: flex; align-items: center; justify-content: flex-start; } #${PANEL_ID} .codex-skill-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 0 0 10px; padding: 8px 10px; border: 1px solid rgba(150, 127, 82, 0.35); background: rgba(10, 10, 9, 0.42); } #${PANEL_ID} .codex-skill-toolbar .codex-skill-controls { display: flex; align-items: center; justify-content: flex-end; gap: 6px; flex-wrap: wrap; padding: 0; border: 0; background: transparent; } #${PANEL_ID} .codex-skill-summary { color: #d4c79d; font-size: 13px; line-height: 1.6; } #${PANEL_ID} .codex-skill-summary { grid-column: 1 / -1; } #${PANEL_ID} .codex-control-group { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; } #${PANEL_ID} .codex-control-group.codex-sort-group { justify-content: flex-end; } #${PANEL_ID} .codex-control-group.codex-lang-group { grid-column: 1 / -1; } #${PANEL_ID} .codex-control-label { color: #8f846a; font-size: 12px; } #${PANEL_ID} .codex-control-group button { padding: 3px 8px; font-size: 12px; } #${PANEL_ID} .codex-skill-body { box-sizing: border-box; padding: 16px 24px 18px; background: #1b2630; max-height: none; overflow: visible; } #${PANEL_ID} .codex-skill-card { box-sizing: border-box; width: 100%; margin: 0 0 10px; padding: 11px 16px 10px; border-radius: 4px; border: 1px solid rgba(159, 179, 198, 0.12); background: #24313d; color: #d9e1e8; } #${PANEL_ID} .codex-skill-card.codex-type-active { box-shadow: inset 0 3px 0 rgba(217, 179, 95, 0.70); } #${PANEL_ID} .codex-skill-card.codex-type-spirit, #${PANEL_ID} .codex-skill-card.codex-type-passive { box-shadow: inset 0 3px 0 rgba(117, 197, 142, 0.70); } #${PANEL_ID} .codex-skill-card.codex-type-granted { box-shadow: inset 0 3px 0 rgba(114, 168, 223, 0.70); } #${PANEL_ID} .codex-skill-top { display: flex; align-items: center; justify-content: flex-start; gap: 16px; margin-bottom: 4px; } #${PANEL_ID} .codex-skill-name { font-size: 16px; font-weight: 600; color: #e0e7ed; } #${PANEL_ID} .codex-skill-name-row { display: flex; align-items: center; gap: 8px; min-width: 0; flex-wrap: wrap; } #${PANEL_ID} .codex-skill-type { flex: none; border: 1px solid currentColor; border-radius: 2px; padding: 1px 6px; font-size: 12px; line-height: 1.4; color: #c8d2dc; background: rgba(255, 255, 255, 0.04); } #${PANEL_ID} .codex-skill-type.codex-type-active { color: #d9b35f; } #${PANEL_ID} .codex-skill-type.codex-type-spirit, #${PANEL_ID} .codex-skill-type.codex-type-passive { color: #75c58e; } #${PANEL_ID} .codex-skill-type.codex-type-granted { color: #72a8df; } #${PANEL_ID} .codex-skill-meta { flex: none; color: #aab7c4; font-size: 13px; padding: 1px 8px; border: 1px solid rgba(159, 179, 198, 0.14); background: rgba(255, 255, 255, 0.035); } #${PANEL_ID} .codex-skill-line { margin-top: 4px; color: #c7d1dc; font-size: 13px; line-height: 1.45; word-break: break-word; } #${PANEL_ID} .codex-skill-label { color: #91a1b0; margin-right: 6px; } #${PANEL_ID} .codex-support-table { width: 100%; margin-top: 8px; border-collapse: collapse; table-layout: fixed; color: #c7d1dc; font-size: 13px; } #${PANEL_ID} .codex-support-table th, #${PANEL_ID} .codex-support-table td { border: 1px solid rgba(159, 179, 198, 0.16); padding: 5px 8px; text-align: left; vertical-align: middle; word-break: break-word; } #${PANEL_ID} .codex-support-table th { color: #91a1b0; font-weight: 400; background: rgba(255, 255, 255, 0.035); } #${PANEL_ID} .codex-item-section { margin: 0 0 18px; padding: 0 12px 12px; border: 1px solid rgba(150, 127, 82, 0.30); border-left: 4px solid rgba(217, 179, 95, 0.92); background: rgba(8, 8, 7, 0.34); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025); } #${PANEL_ID} .codex-item-section-title { display: flex; align-items: baseline; justify-content: flex-start; gap: 10px; margin: 0 -12px 12px; padding: 9px 12px 9px 10px; border-bottom: 1px solid rgba(150, 127, 82, 0.28); background: rgba(150, 127, 82, 0.14); color: #d4c79d; font-size: 17px; font-weight: 700; } #${PANEL_ID} .codex-section-title-main { display: flex; align-items: baseline; gap: 10px; min-width: 0; flex-wrap: wrap; } #${PANEL_ID} .codex-section-title-text { color: #f0d99a; font-size: 17px; font-weight: 700; } #${PANEL_ID} .codex-item-section-title span { color: #8f9aa5; font-size: 12px; font-weight: 400; } #${PANEL_ID} .codex-collapse-button { flex: none; margin-left: 2px; padding: 2px 8px; font-size: 12px; line-height: 1.5; background: rgba(150, 127, 82, 0.18); border-color: rgba(217, 179, 95, 0.42); } #${PANEL_ID} .codex-item-section.codex-collapsed > :not(.codex-item-section-title) { display: none !important; } #${PANEL_ID} .codex-item-section.codex-collapsed { padding-bottom: 0; } #${PANEL_ID} .codex-item-section.codex-collapsed .codex-item-section-title { margin-bottom: 0; border-bottom: 0; } #${PANEL_ID} .codex-item-table { width: 100%; margin: 0; border-collapse: collapse; table-layout: fixed; color: #c7d1dc; font-size: 13px; } #${PANEL_ID} .codex-item-table th, #${PANEL_ID} .codex-item-table td { border: 1px solid rgba(159, 179, 198, 0.16); padding: 6px 8px; text-align: left; vertical-align: top; word-break: break-word; } #${PANEL_ID} .codex-item-table th { color: #91a1b0; font-weight: 400; background: rgba(255, 255, 255, 0.035); } #${PANEL_ID} .codex-item-index { width: 42px; text-align: center !important; } #${PANEL_ID} .codex-item-slot { width: 92px; } #${PANEL_ID} .codex-item-rarity { width: 72px; } #${PANEL_ID} .codex-rarity-unique { color: #af6025; font-weight: 700; } #${PANEL_ID} .codex-rarity-rare { color: #d9c75f; font-weight: 700; } #${PANEL_ID} .codex-rarity-magic { color: #8888ff; font-weight: 700; } #${PANEL_ID} .codex-rarity-normal { color: #c7d1dc; } #${PANEL_ID} .codex-item-props, #${PANEL_ID} .codex-item-socketed { width: 180px; } #${PANEL_ID} .codex-cell-list { display: grid; gap: 3px; } #${PANEL_ID} .codex-list-row { display: grid; grid-template-columns: 28px minmax(0, 1fr); gap: 4px; } #${PANEL_ID} .codex-list-marker, #${PANEL_ID} .codex-mod-label { color: #d9b35f; } #${PANEL_ID} .codex-mod-group + .codex-mod-group { margin-top: 6px; padding-top: 6px; border-top: 1px solid rgba(159, 179, 198, 0.12); } #${PANEL_ID} .codex-support-index { width: 46px; text-align: center !important; } #${PANEL_ID} .codex-support-type { width: 72px; text-align: center !important; } #${PANEL_ID} td.codex-support-type { font-weight: 700; } #${PANEL_ID} td.codex-support-type-active { color: #d9b35f; } #${PANEL_ID} td.codex-support-type-passive { color: #75c58e; } #${PANEL_ID} td.codex-support-type-lineage { color: #c99cff; } #${PANEL_ID} td.codex-support-type-spirit { color: #8fd8a4; } #${PANEL_ID} .codex-support-level, #${PANEL_ID} .codex-support-quality { width: 120px; } #${PANEL_ID} .codex-support-details { width: 180px; } #${PANEL_ID} .codex-support-requirements { width: 180px; } #${PANEL_ID} pre.codex-skill-plain { margin: 0; white-space: pre-wrap; word-break: break-word; font: 13px/1.55 ui-monospace, SFMono-Regular, Consolas, "Microsoft YaHei UI", monospace; color: #d9e4ef; } #${PANEL_ID} .codex-skill-status { box-sizing: border-box; display: none; padding: 0; color: #9fb3c6; font: 12px/1.4 "Microsoft YaHei UI", sans-serif; } #${PANEL_ID}.codex-skill-panel-mounted .codex-skill-header { min-height: 44px; padding: 0 16px; background: rgba(19, 20, 19, 0.88); border: 1px solid rgba(150, 127, 82, 0.35); border-bottom: 0; border-radius: 0; color: #b9aa88; } #${PANEL_ID}.codex-skill-panel-mounted .codex-skill-title { font-size: 18px; color: #b9aa88; } #${PANEL_ID}.codex-skill-panel-mounted button { border-color: rgba(150, 127, 82, 0.4); background: rgba(32, 30, 24, 0.9); color: #b9aa88; padding: 3px 9px; } #${PANEL_ID}.codex-skill-panel-mounted button:hover { background: rgba(46, 41, 30, 0.95); } #${PANEL_ID}.codex-skill-panel-mounted button.codex-active-control { border-color: #d9b35f; color: #f0d99a; background: rgba(150, 127, 82, 0.28); box-shadow: inset 0 0 0 1px rgba(240, 217, 154, 0.18); } #${PANEL_ID}.codex-skill-panel-mounted .codex-skill-body { padding: 10px 12px 2px; background: rgba(8, 9, 9, 0.78); border: 1px solid rgba(150, 127, 82, 0.35); border-top: 0; max-height: none; overflow: visible; } #${PANEL_ID}.codex-skill-panel-mounted .codex-skill-card { margin-bottom: 8px; padding: 9px 12px 8px; border: 1px solid rgba(150, 127, 82, 0.22); border-radius: 0; background: rgba(26, 26, 22, 0.82); color: #b9aa88; } #${PANEL_ID}.codex-skill-panel-mounted .codex-skill-card.codex-type-active { box-shadow: inset 0 3px 0 rgba(217, 179, 95, 0.70); } #${PANEL_ID}.codex-skill-panel-mounted .codex-skill-card.codex-type-spirit, #${PANEL_ID}.codex-skill-panel-mounted .codex-skill-card.codex-type-passive { box-shadow: inset 0 3px 0 rgba(117, 197, 142, 0.70); } #${PANEL_ID}.codex-skill-panel-mounted .codex-skill-card.codex-type-granted { box-shadow: inset 0 3px 0 rgba(114, 168, 223, 0.70); } #${PANEL_ID}.codex-skill-panel-mounted .codex-skill-name { color: #d4c79d; font-size: 15px; } #${PANEL_ID}.codex-skill-panel-mounted .codex-skill-meta { border-color: rgba(150, 127, 82, 0.24); background: rgba(150, 127, 82, 0.06); } #${PANEL_ID}.codex-skill-panel-mounted .codex-support-table { color: #a99d7d; } #${PANEL_ID}.codex-skill-panel-mounted .codex-item-table { color: #a99d7d; } #${PANEL_ID}.codex-skill-panel-mounted .codex-support-table th, #${PANEL_ID}.codex-skill-panel-mounted .codex-support-table td, #${PANEL_ID}.codex-skill-panel-mounted .codex-item-table th, #${PANEL_ID}.codex-skill-panel-mounted .codex-item-table td { border-color: rgba(150, 127, 82, 0.28); } #${PANEL_ID}.codex-skill-panel-mounted .codex-support-table th, #${PANEL_ID}.codex-skill-panel-mounted .codex-item-table th { color: #d4c79d; background: rgba(150, 127, 82, 0.08); } #${PANEL_ID}.codex-skill-panel-mounted .codex-skill-meta, #${PANEL_ID}.codex-skill-panel-mounted .codex-skill-line, #${PANEL_ID}.codex-skill-panel-mounted .codex-skill-label, #${PANEL_ID}.codex-skill-panel-mounted .codex-skill-status { color: #a99d7d; } #${PANEL_ID}.codex-skill-panel-mounted .codex-skill-status { display: none; padding: 0; background: rgba(8, 9, 9, 0.78); } `; document.head.appendChild(style); } function findInlineMountPoint() { const skillPanel = document.querySelector(".skill-panel"); if (skillPanel?.parentElement) return skillPanel; const skillContent = document.querySelector(".skill-panel .skill-content, .skill-content"); if (skillContent) return skillContent; const bdSkillBlock = [...document.querySelectorAll(".bd-content-inner, .skill-panel, .bd-section")] .find((element) => /(等级|最高等级|技能DPS|技能)/.test(element.innerText || "")); if (bdSkillBlock) return bdSkillBlock; return document.body; } function mountPanel(panel) { const mount = findInlineMountPoint(); const isSkillPanelMount = Boolean(mount?.classList?.contains("skill-panel")); panel.classList.toggle("codex-skill-panel-mounted", isSkillPanelMount); if (!mount || mount === document.body) { panel.classList.remove("codex-skill-panel-mounted"); panel.classList.add("codex-body-mounted"); document.body.appendChild(panel); return; } panel.classList.remove("codex-body-mounted"); if (isSkillPanelMount) { if (panel.previousElementSibling === mount) return; mount.insertAdjacentElement("afterend", panel); return; } if (panel.parentElement === mount && panel === mount.lastElementChild) return; mount.appendChild(panel); } function expandSkillHost(panel) { const skillPanel = document.querySelector(".skill-panel"); if (!panel || !skillPanel || panel.previousElementSibling !== skillPanel) return; const hosts = [ skillPanel.parentElement, skillPanel.closest(".bd-content-inner"), skillPanel.closest(".bd-content"), skillPanel.closest(".bd-section"), skillPanel.closest(".share-section"), ].filter(Boolean); for (const host of [...new Set(hosts)]) { if (!host.contains(panel)) continue; const hostRect = host.getBoundingClientRect(); const panelRect = panel.getBoundingClientRect(); const neededHeight = Math.ceil(panelRect.bottom - hostRect.top + 120); host.style.height = "auto"; host.style.minHeight = `${Math.max(host.offsetHeight, host.scrollHeight, neededHeight)}px`; host.style.overflow = "visible"; } } async function copyText(value) { const text = value || ""; if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(text); return true; } catch (_) { // Fall through to userscript/native textarea copy. } } if (typeof GM_setClipboard === "function") { try { GM_setClipboard(text, "text"); return true; } catch (_) { // Fall through to textarea copy. } } const textarea = document.createElement("textarea"); textarea.value = text; textarea.setAttribute("readonly", "readonly"); textarea.style.position = "fixed"; textarea.style.left = "-9999px"; textarea.style.top = "0"; document.body.appendChild(textarea); textarea.select(); textarea.setSelectionRange(0, textarea.value.length); const ok = document.execCommand("copy"); textarea.remove(); return ok; } function summarizeRows(rows) { return rows.reduce((summary, row) => { summary.total += 1; if (row.skillType === "主动技能") summary.active += 1; if (row.skillType === "被动技能") summary.passive += 1; if (row.skillType === "精魂技能") summary.spirit += 1; return summary; }, { total: 0, active: 0, passive: 0, spirit: 0 }); } function filterRows(rows) { if (currentFilter === "active") return rows.filter((row) => row.skillType === "主动技能"); if (currentFilter === "passive") return rows.filter((row) => row.skillType === "被动技能"); if (currentFilter === "spirit") return rows.filter((row) => row.skillType === "精魂技能"); return [...rows]; } function sortedRows(rows) { const result = [...rows]; if (currentSort === "type") { const order = { "主动技能": 0, "被动技能": 1, "精魂技能": 2, "装备赋予": 3 }; result.sort((a, b) => (order[a.skillType] ?? 9) - (order[b.skillType] ?? 9) || a.originalIndex - b.originalIndex); } return result; } function visibleRows(rows) { if (!["all", "active", "passive", "spirit"].includes(currentFilter)) currentFilter = "all"; if (!["original", "type"].includes(currentSort)) currentSort = "original"; return sortedRows(filterRows(rows)); } function scrollSnapshot() { const elements = [document.scrollingElement, document.documentElement, document.body, ...document.querySelectorAll("*")] .filter((element, index, list) => element && list.indexOf(element) === index) .filter((element) => element.scrollTop || element.scrollLeft || element.scrollHeight > element.clientHeight + 2 || element.scrollWidth > element.clientWidth + 2) .map((element) => ({ element, top: element.scrollTop, left: element.scrollLeft })); return { x: window.scrollX, y: window.scrollY, elements }; } function restoreScrollPosition(snapshot) { window.scrollTo(snapshot.x, snapshot.y); for (const item of snapshot.elements) { if (!item.element?.isConnected && item.element !== document.body && item.element !== document.documentElement) continue; item.element.scrollTop = item.top; item.element.scrollLeft = item.left; } } function restoreScrollRepeatedly(snapshot) { restoreScrollPosition(snapshot); requestAnimationFrame(() => restoreScrollPosition(snapshot)); setTimeout(() => restoreScrollPosition(snapshot), 0); setTimeout(() => restoreScrollPosition(snapshot), 80); setTimeout(() => restoreScrollPosition(snapshot), 240); setTimeout(() => restoreScrollPosition(snapshot), 600); setTimeout(() => restoreScrollPosition(snapshot), 1200); const startedAt = Date.now(); const lock = setInterval(() => { restoreScrollPosition(snapshot); if (Date.now() - startedAt > 1200) clearInterval(lock); }, 50); } async function preserveScroll(callback) { const snapshot = lastPanelScrollSnapshot || scrollSnapshot(); try { return await callback(); } finally { restoreScrollRepeatedly(snapshot); lastPanelScrollSnapshot = null; } } function panelButtonFromEvent(event) { return event.target?.closest?.(`#${PANEL_ID} button`) || null; } function stopPanelPointerEvent(event) { if (!panelButtonFromEvent(event)) return; lastPanelScrollSnapshot = scrollSnapshot(); event.stopPropagation(); event.stopImmediatePropagation(); if (event.type === "pointerdown" || event.type === "mousedown") { event.preventDefault(); } } async function handlePanelButton(button) { const panel = document.getElementById(PANEL_ID); if (button.dataset.collapseSection) { const key = button.dataset.collapseSection; setSectionCollapsed(key, !isSectionCollapsed(key)); button.blur(); await preserveScroll(() => renderRowsIntoPanel(panel)); return; } if (button.dataset.nameLang) { currentNameLang = button.dataset.nameLang; button.blur(); await preserveScroll(async () => { await ensureNameLanguageLoaded(); updateRenderedLanguage(panel); }); return; } if (button.dataset.filter) { currentFilter = button.dataset.filter; button.blur(); await preserveScroll(() => renderRowsIntoPanel(panel)); return; } if (button.dataset.sort) { currentSort = button.dataset.sort; button.blur(); await preserveScroll(() => renderRowsIntoPanel(panel)); return; } if (button.dataset.action === "refresh") { button.blur(); await preserveScroll(() => refresh()); return; } if (button.dataset.action === "copy") { const value = currentCopyText(); const ok = await copyText(value); button.textContent = ok ? "已复制" : "复制失败"; setTimeout(() => { button.textContent = "复制"; }, 1200); } } function startPanelEventGuard() { if (panelEventGuardStarted) return; panelEventGuardStarted = true; ["pointerdown", "mousedown", "mouseup"].forEach((type) => { window.addEventListener(type, stopPanelPointerEvent, true); }); window.addEventListener("click", (event) => { const button = panelButtonFromEvent(event); if (!button) return; lastPanelScrollSnapshot = lastPanelScrollSnapshot || scrollSnapshot(); event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); handlePanelButton(button); }, true); } function updateControls(panel, rows, shownRows) { const summary = summarizeRows(rows); const summaryEl = panel.querySelector(".codex-skill-summary"); if (summaryEl) { summaryEl.textContent = `总技能 ${summary.total} | 主动 ${summary.active} | 被动 ${summary.passive}${summary.spirit ? ` | 精魂 ${summary.spirit}` : ""}`; } panel.querySelectorAll("[data-filter]").forEach((button) => { button.classList.toggle("codex-active-control", button.dataset.filter === currentFilter); }); panel.querySelectorAll("[data-sort]").forEach((button) => { button.classList.toggle("codex-active-control", button.dataset.sort === currentSort); }); panel.querySelectorAll("[data-name-lang]").forEach((button) => { button.classList.toggle("codex-active-control", button.dataset.nameLang === currentNameLang); button.disabled = nameLangState === "loading"; const baseLabel = NAME_LANG_LABELS[button.dataset.nameLang] || button.textContent; button.textContent = nameLangState === "loading" && button.dataset.nameLang === currentNameLang ? `${baseLabel}...` : baseLabel; }); const title = panel.querySelector(".codex-skill-title"); if (title) title.title = nameLangMessage || ""; } function updateRenderedLanguage(panel = document.getElementById(PANEL_ID)) { if (!panel) return; renderRowsIntoPanel(panel); } function startMountWatcher() { if (mountWatcherStarted) return; mountWatcherStarted = true; setInterval(() => { const panel = document.getElementById(PANEL_ID); const skillPanel = document.querySelector(".skill-panel"); if (panel && skillPanel) { mountPanel(panel); } }, 1500); } function renderCellList(items) { const list = (items || []).map(cleanText).filter(Boolean); if (!list.length) return "-"; return `
${list.map((item, index) => `
${index + 1}. ${escapeHtml(item)}
`).join("")}
`; } function renderModGroups(groups) { if (!groups?.length) return "-"; return groups.map((group) => `
${escapeHtml(group.label)}
${renderCellList(group.lines)}
`).join(""); } function renderItemSection(body, title, rows, isJewel = false, collapseKey = "") { if (!rows.length) return; const section = document.createElement("section"); section.className = "codex-item-section"; section.innerHTML = sectionTitleHtml(title, `${rows.length} 个`, collapseKey); const table = document.createElement("table"); table.className = "codex-item-table"; table.innerHTML = ` # ${isJewel ? "位置" : "部位"} 名称 稀有度 属性/需求 插槽物 词缀 `; const tbody = table.querySelector("tbody"); rows.forEach((row, index) => { const status = itemStatusText(row); const rarityClassName = rarityClass(row.rarity); const tr = document.createElement("tr"); tr.innerHTML = ` ${index + 1} ${escapeHtml(isJewel ? (row.position || row.slot || "-") : row.slot)} ${escapeHtml(`${localizedName(row.name)}${status ? ` (${status})` : ""}`)} ${escapeHtml(row.rarity || "-")} ${renderCellList(splitListText(row.properties))} ${renderCellList(row.socketed.map(localizedName))} ${renderModGroups(row.mods)} `; tbody.appendChild(tr); }); section.appendChild(table); applySectionCollapsed(section, collapseKey); body.appendChild(section); } function renderBuildInfoSection(body) { if (!lastBuildInfo) return; const section = document.createElement("section"); section.className = "codex-item-section"; const rows = [ ["角色", lastBuildInfo.roleTitle], ["BD标题", lastBuildInfo.title], ["BD时间", lastBuildInfo.timeBd], ].filter(([, value]) => value); section.innerHTML = ` ${sectionTitleHtml("基本信息", `${rows.length} 项`)} ${rows.map(([label, value]) => ` `).join("")}
${escapeHtml(label)} ${escapeHtml(value)}
`; body.appendChild(section); } function renderTalentSection(body) { if (!lastTalent) return; const section = document.createElement("section"); section.className = "codex-item-section"; const summary = [ lastTalent.assigned ? `天赋点 ${lastTalent.assigned}` : "", `武器1 ${lastTalent.set1Count || 0}`, `武器2 ${lastTalent.set2Count || 0}`, lastTalent.jewelSocketCount ? `天赋珠宝槽 ${lastTalent.jewelSocketCount}` : "", ].filter(Boolean).join(" | "); section.innerHTML = ` ${sectionTitleHtml("天赋/任务奖励", summary || "-", "talent")}
天赋 ${escapeHtml(summary || "-")}
任务奖励 ${renderCellList(lastTalent.questStats || [])}
`; applySectionCollapsed(section, "talent"); body.appendChild(section); } function renderSkillSection(body, rowsToRender) { const section = document.createElement("section"); section.className = "codex-item-section codex-skill-section"; const summary = summarizeRows(lastRows); section.innerHTML = ` ${sectionTitleHtml("技能", `${rowsToRender.length} 个`, "skills")}
总技能 ${summary.total} | 主动 ${summary.active} | 被动 ${summary.passive}${summary.spirit ? ` | 精魂 ${summary.spirit}` : ""}
筛选 排序
`; body.appendChild(section); applySectionCollapsed(section, "skills"); return section; } function renderRowsIntoPanel(panel) { const body = panel.querySelector(".codex-skill-body"); if (!body) return; body.innerHTML = ""; const rowsToRender = visibleRows(lastRows); renderBuildInfoSection(body); renderItemSection(body, "装备/药剂/护符", lastEquipment, false, "equipment"); renderItemSection(body, "珠宝", lastJewels, true, "jewels"); renderTalentSection(body); const skillSection = renderSkillSection(body, rowsToRender); updateControls(panel, lastRows, rowsToRender); if (lastRows.length) { if (!rowsToRender.length) { const card = document.createElement("div"); card.className = "codex-skill-card"; card.textContent = "当前筛选没有匹配技能。"; skillSection.appendChild(card); } for (const row of rowsToRender) { const card = document.createElement("div"); card.className = `codex-skill-card ${row.skillTypeClass || ""}`.trim(); card.dataset.rowIndex = String(row.originalIndex); card.innerHTML = `
`; const typeBadge = card.querySelector(".codex-skill-type"); typeBadge.textContent = row.skillType || "主动技能"; if (row.skillTypeClass) typeBadge.classList.add(row.skillTypeClass); card.querySelector(".codex-skill-name").textContent = localizedName(row.name); card.querySelector(".codex-skill-meta").textContent = `Lv${row.level} / ${row.quality} / ${row.sockets}孔`; const tags = card.querySelector(".codex-skill-tags"); tags.innerHTML = `标签${row.tags || "-"}`; const requirements = card.querySelector(".codex-skill-requirements"); if (row.requirements) { requirements.innerHTML = `需求${row.requirements}`; } else { requirements.remove(); } const details = card.querySelector(".codex-skill-details"); if (row.details?.length) { details.innerHTML = `属性${row.details.join(" / ")}`; } else { details.remove(); } const supports = card.querySelector(".codex-skill-supports"); const tableRows = insertedRows(row); supports.innerHTML = `已插入技能${tableRows.length} 个`; if (tableRows.length) { const table = document.createElement("table"); table.className = "codex-support-table"; table.innerHTML = ` # 类型 技能石 等级 品质 属性 需求 `; const tbody = table.querySelector("tbody"); tableRows.forEach((support, supportIndex) => { const tr = document.createElement("tr"); const indexTd = document.createElement("td"); indexTd.className = "codex-support-index"; indexTd.textContent = String(supportIndex + 1); const typeTd = document.createElement("td"); const rowType = support.rowType || "被动"; const typeClass = rowType === "主动" ? "active" : rowType === "血脉" ? "lineage" : rowType === "精魂" ? "spirit" : "passive"; typeTd.className = `codex-support-type codex-support-type-${typeClass}`; typeTd.textContent = rowType; const nameTd = document.createElement("td"); nameTd.className = "codex-support-name"; nameTd.textContent = localizedName(support.name || "-"); const levelTd = document.createElement("td"); levelTd.className = "codex-support-level"; levelTd.textContent = support.level && support.level !== "-" ? `Lv${support.level}` : "-"; const qualityTd = document.createElement("td"); qualityTd.className = "codex-support-quality"; qualityTd.textContent = support.quality || (rowType === "主动" ? "品质0%" : "-"); const detailsTd = document.createElement("td"); detailsTd.className = "codex-support-details"; detailsTd.textContent = support.details?.length ? support.details.join(" / ") : "-"; const requirementsTd = document.createElement("td"); requirementsTd.className = "codex-support-requirements"; requirementsTd.textContent = support.requirements || "-"; tr.append(indexTd, typeTd, nameTd, levelTd, qualityTd, detailsTd, requirementsTd); tbody.appendChild(tr); }); supports.appendChild(table); } const nested = card.querySelector(".codex-skill-nested"); nested.remove(); skillSection.appendChild(card); } } else { const card = document.createElement("div"); card.className = "codex-skill-card"; const pre = document.createElement("pre"); pre.className = "codex-skill-plain"; pre.textContent = lastText || ""; card.appendChild(pre); skillSection.appendChild(card); } panel.querySelector(".codex-skill-status").textContent = ""; } function renderPanel(result, status) { ensureStyle(); if (typeof result === "string") { lastText = result || ""; lastRows = []; lastEquipment = []; lastJewels = []; lastTalent = null; lastBuildInfo = null; } else { lastText = result?.text || ""; lastRows = result?.rows || []; lastEquipment = result?.equipment || []; lastJewels = result?.jewels || []; lastTalent = result?.talent || null; lastBuildInfo = result?.buildInfo || null; } let panel = document.getElementById(PANEL_ID); if (!panel) { panel = document.createElement("div"); panel.id = PANEL_ID; panel.innerHTML = `
BD信息 脚本更新:${SCRIPT_UPDATED_AT}
语言
`; startPanelEventGuard(); ["pointerdown", "mousedown", "mouseup"].forEach((type) => { panel.addEventListener(type, stopPanelPointerEvent, true); }); panel.addEventListener("click", async (event) => { const button = event.target.closest("button"); if (!button) return; event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); await handlePanelButton(button); }); } mountPanel(panel); renderRowsIntoPanel(panel); requestAnimationFrame(() => expandSkillHost(panel)); } async function refresh() { const shareCode = extractShareCode(); lastShareCode = shareCode; if (!shareCode) { const fallback = domFallbackText(); renderPanel(fallback || "未识别到分享码。请确认当前 URL 是 #/share/... 页面。", "未识别分享码,使用页面文本兜底。"); return; } renderPanel(lastText || "正在读取 BD 数据...", "读取中"); try { const roleInfo = await postJson("GetRoleInfo", roleBody(shareCode)); const role = roleInfo?.role || {}; const body = roleBody(shareCode, role); const [skillsResult, equipmentsResult, jewelsResult, talentResult, profileResult, summaryResult] = await Promise.allSettled([ postJson("GetSkills", body), postJson("GetEquipments", body), postJson("GetJewels", body), postJson("GetTalentTree", body), postJson("GetRoleProfile", body), postJson("GetRoleSummary", body), ]); if (skillsResult.status !== "fulfilled") throw skillsResult.reason; const formatted = formatSkills(skillsResult.value, role); formatted.equipment = equipmentsResult.status === "fulfilled" ? normalizeEquipments(equipmentsResult.value) : []; formatted.jewels = jewelsResult.status === "fulfilled" ? normalizeJewels(jewelsResult.value) : []; formatted.talent = normalizeTalent( talentResult.status === "fulfilled" ? talentResult.value : null, profileResult.status === "fulfilled" ? profileResult.value : null ); formatted.buildInfo = normalizeBuildInfo(summaryResult.status === "fulfilled" ? summaryResult.value : null, role); renderPanel(formatted, "BD数据已更新"); } catch (error) { const fallback = domFallbackText(); renderPanel( fallback || `接口读取失败:${error?.message || error}`, fallback ? `接口读取失败,已使用页面文本兜底:${error?.message || error}` : "接口读取失败" ); } } function watchRoute() { let lastHref = location.href; setInterval(() => { if (location.href === lastHref) return; lastHref = location.href; const currentShareCode = extractShareCode(); if (currentShareCode && currentShareCode !== lastShareCode) { refresh(); } }, 1000); } refresh(); startMountWatcher(); watchRoute(); })();