// ==UserScript== // @name 石之家-修为查询 // @namespace http://tampermonkey.net/ // @version 1.2.2 // @description 鼠标悬停至水晶icon上即可查询,查询拥有24小时缓存。点击则直接跳转FFLOGS查询页面。 // @author Souma // @match *://ff14risingstones.web.sdo.com/* // @icon <$ICON$> // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_setValue // @grant GM_getValue // @license MIT // @downloadURL none // ==/UserScript== (function () { "use strict"; const jobsCN = { "Adventurer": "冒险", "Gladiator": "剑术", "Pugilist": "格斗", "Marauder": "斧术", "Lancer": "枪术", "Archer": "弓箭", "Conjurer": "幻术", "Thaumaturge": "咒术", "Carpenter": "刻木", "Blacksmith": "锻铁", "Armorer": "铸甲", "Goldsmith": "雕金", "Leatherworker": "制革", "Weaver": "裁衣匠", "Alchemist": "炼金", "Culinarian": "烹调", "Miner": "采矿", "Botanist": "园艺", "Fisher": "捕鱼", "Paladin": "骑士", "Monk": "武僧", "Warrior": "战士", "Dragoon": "龙骑", "Bard": "诗人", "White Mage": "白魔", "Black Mage": "黑魔", "Arcanist": "秘术", "Summoner": "召唤", "Scholar": "学者", "Rogue": "双剑", "Ninja": "忍者", "Machinist": "机工", "Dark Knight": "暗骑", "Astrologian": "占星", "Samurai": "武士", "Red Mage": "赤魔", "Blue Mage": "青魔", "Gunbreaker": "绝枪", "Dancer": "舞者", "Reaper": "钐镰", "Sage": "贤者", }; const getPerHTML = (per) => { if (per === 100) return `${per}`; //金色 per = Math.round(per); if (per >= 99) return `${per}`; //粉色 if (per >= 95) return `${per}`; //橙色 if (per >= 75) return `${per}`; //紫色 if (per >= 50) return `${per}`; //蓝色 if (per >= 25) return `${per}`; //绿色 else return `${per}`; //灰色 }; const STORAGE_KEY_LOGS = "szj-logs"; const STORAGE_KEY_API_KEY = "zj-api-key"; const cacheMax = 100; let api_key = GM_getValue(STORAGE_KEY_API_KEY, ""); const getApiKey = () => { window.open("https://cn.fflogs.com/profile#api-title", "_blank"); }; const setApiKey = () => { const newKey = prompt("请输入 V1 Client Key: ", api_key); if (newKey !== null) { GM_setValue(STORAGE_KEY_API_KEY, newKey); api_key = GM_getValue(STORAGE_KEY_API_KEY, ""); } }; GM_registerMenuCommand("获得API_KEY", getApiKey); GM_registerMenuCommand("设置API_KEY", setApiKey); let cache = localStorage.getItem(STORAGE_KEY_LOGS) ? JSON.parse(localStorage.getItem(STORAGE_KEY_LOGS)) : {}; // 清理缓存 for (const key in cache) { const item = cache[key]; if (item && item.time && item.time < Date.now() - 1000 * 60 * 60 * 24) { delete cache[key]; } } const errorMap = { "Invalid character name/server/region specified.": "未找到该角色", }; const targetNode = document.body; const config = { attributes: true, childList: true, subtree: true, }; const observer = new MutationObserver(callback); observer.observe(targetNode, config); function callback(mutationsList, _observer) { for (const mutation of mutationsList) { if (mutation.type === "childList") { mutation.addedNodes.forEach(function (addedNode) { if (addedNode.nodeType === 1 && addedNode.tagName === "DIV" && !addedNode.matches(".mt10")) { const node = addedNode.querySelector(".mt10>.el-row>.el-col>.alcenter") || addedNode.querySelector(".detail")?.querySelector(".mt3.flex.alcenter") || addedNode.querySelector(".flex>.info-main"); if (!node) return; const title = addedNode.querySelector(".mt3.flex.alcenter"); const name = node.querySelector(".name>span")?.innerText || node.querySelector(".ft24.ftw")?.innerText || title?.querySelector(".cursor")?.innerText; const group = node.querySelector(".line>.group")?.innerText || node.querySelector(".graycolor")?.children?.[1]?.innerText || title?.querySelector(".graycolor")?.children?.[1]?.innerText; if (!name || !group) return; let fetching = false; const div = document.createElement("div"); const img = document.createElement("img"); const info = document.createElement("span"); div.appendChild(img); node.appendChild(div); div.appendChild(info); img.src = "https://assets.rpglogs.cn/img/ff/favicon.png"; img.style.height = "20px"; div.style.cursor = "pointer"; div.style.display = "inline-block"; div.onclick = () => window.open(`https://cn.fflogs.com/character/CN/${group}/${name}`, "_blank"); const c = cache[`${name}/${group}`]; if (c && c.data && Date.now() - c.time < 1000 * 60 * 60 * 24) { try { create(JSON.parse(c.data)); } catch { query(); } } else { query(); } function query() { delete cache[`${name}/${group}`]; img.addEventListener("mouseenter", function () { if (api_key === "") { setApiKey(); return; } if (fetching) return; fetching = true; info.innerText = "查询中..."; fetch( `https://www.fflogs.com/v1/rankings/character/${name}/${encodeURI( group )}/CN?metric=rdps&timeframe=historical&api_key=${api_key}` ) .then((v) => v.json()) .then((v) => { if (v.error) { info.innerText = errorMap[v.error] ?? v.error; return; } create(v); }) .catch((e) => { info.innerText = "失败"; console.error(e); }); }); } function create(v) { if (v.hidden) { info.innerText = "隐藏"; cache[`${name}/${group}`] = { data: JSON.stringify({ hidden: true }), time: Date.now() }; } else { if (!(v instanceof Array)) { console.error("未知错误", v); info.innerText = "未知错误"; return; } const svg = v.filter((v) => v.difficulty !== 100); if (svg.length === 0) { info.innerText = "无数据"; } else { info.innerText = ""; const res = {}; svg.forEach((s) => (res[s.spec] ??= []).push(s.percentile)); let waitingForAppend = []; for (const job in res) { const pers = res[job]; const avg_per = pers.reduce((p, c) => p + c, 0) / pers.length; const article = document.createElement("span"); const job_dom = document.createElement("span"); const per_dom = document.createElement("span"); job_dom.innerText = jobsCN[job] ?? job; per_dom.style.padding = "0 0.2em"; per_dom.innerHTML = getPerHTML(avg_per); article.setAttribute("data-avg-per", avg_per); article.setAttribute("data-pers-length", pers.length); article.appendChild(job_dom); article.appendChild(per_dom); waitingForAppend.push(article); } waitingForAppend.sort((a, b) => { if (a.dataset.avgPer === b.dataset.avgPer) return a.dataset.persLength - b.dataset.persLength; return +b.dataset.avgPer - +a.dataset.avgPer; }); waitingForAppend.slice(0, 3).map((w) => info.appendChild(w)); } const saveSvg = svg.map(({ encounterID, spec, difficulty, percentile }) => ({ encounterID, spec, difficulty, percentile, })); cache[`${name}/${group}`] = { data: JSON.stringify(saveSvg), time: Date.now() }; } // 只保留最新的50条,防止缓存过大 if (Object.keys(cache).length > cacheMax) { cache = Object.fromEntries(Object.entries(cache).slice(0 - cacheMax)); } localStorage.setItem(STORAGE_KEY_LOGS, JSON.stringify(cache)); } } }); } } } // observer.disconnect(); })();