// ==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();
})();