Greasy Fork

Greasy Fork is available in English.

石之家-修为查询

鼠标悬停至水晶icon上即可查询,查询拥有24小时缓存。点击则直接跳转FFLOGS查询页面。

当前为 2023-12-19 提交的版本,查看 最新版本

// ==UserScript==
// @name         石之家-修为查询
// @namespace    http://tampermonkey.net/
// @version      1.2.1
// @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

// ==/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 `<span style='color:#e5cc80'>${per}</span>`; //金色
    per = Math.round(per);
    if (per >= 99) return `<span style='color:#e268a8'>${per}</span>`; //粉色
    if (per >= 95) return `<span style='color:#ff8000'>${per}</span>`; //橙色
    if (per >= 75) return `<span style='color:#a335ee'>${per}</span>`; //紫色
    if (per >= 50) return `<span style='color:#0070ff'>${per}</span>`; //蓝色
    if (per >= 25) return `<span style='color:#1eff00'>${per}</span>`; //绿色
    else return `<span style='color:#666'>${per}</span>`; //灰色
  };

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