FF Progs

Adds a Button to view logs in xivanalysis also some minor improvements.

当前为 2023-05-02 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name                FF Progs
// @name:en             FF Progs
// @description         Adds a Button to view logs in xivanalysis also some minor improvements.
// @description:en      Adds a Button to view logs in xivanalysis also some minor improvements.
// @version             1.0.4
// @namespace           k_fizzel
// @author              Chad Bradly
// @website             https://www.fflogs.com/character/id/12781922
// @icon                https://assets.rpglogs.com/img/ff/favicon.png?v=2
// @match               https://*.fflogs.com/*
// @require             https://code.jquery.com/jquery-3.2.0.min.js
// @grant               unsafeWindow
// @grant               GM_addStyle
// @grant               GM_getValue
// @grant               GM_setValue
// @grant               GM_deleteValue
// @license             MIT License
// ==/UserScript==

(function () {
  "use strict";
  if (/fflogs\.com/.test(location.hostname)) {
    const JOB_ORDER = [
      // Tanks
      "Paladin",
      "Warrior",
      "DarkKnight",
      "Gunbreaker",
      // Healers
      "WhiteMage",
      "Scholar",
      "Astrologian",
      "Sage",
      // Melee
      "Monk",
      "Dragoon",
      "Ninja",
      "Samurai",
      "Reaper",
      // Physical Ranged
      "Bard",
      "Machinist",
      "Dancer",
      // Magical Ranged
      "BlackMage",
      "Summoner",
      "RedMage",
    ];

    const ABILITY_TYPES = {
      0: "None",
      1: "Buff",
      2: "Unknown",
      4: "Unknown",
      8: "Heal",
      16: "Unknown",
      32: "True",
      64: "DOT",
      124: "Darkness",
      125: "Darkness",
      126: "Darkness",
      127: "Darkness",
      128: "Physical",
      256: "Magical",
      512: "Unknown",
      1024: "Magical",
    };

    const getHashParams = () => {
      const hash = window.location.hash.substring(1);
      const params = {};

      hash.split("&").forEach((pair) => {
        const [key, value] = pair.split("=");
        params[key] = decodeURIComponent(value);
      });

      return params;
    };

    const changeHashParams = (defaultParams, params) => {
      const hashParams = params || getHashParams();
      const newParams = {
        ...hashParams,
        ...defaultParams,
      };

      location.hash = Object.entries(newParams)
        .filter(([key, value]) => (value === "undefined" || value === "null" || value === "" || value === null || value === undefined ? false : true))
        .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
        .join("&");
    };

    const characterAllStar = (rank, outOf, rDPS, rankOneRDPS) => {
      return Math.min(Math.max(100 * (rDPS / rankOneRDPS), 100 - (rank / outOf) * 100) + 20 * (rDPS / rankOneRDPS), 120);
    };

    // Remove Ads
    $("#top-banner, .side-rail-ads, #bottom-banner, #subscription-message-tile-container, #playwire-video-container, #right-ad-box, #right-vertical-banner").remove();
    $("#table-container").css("margin", "0 0 0 0");

    if (/\/reports\/.+/.test(location.pathname)) {
      // Add XIV Analysis Button
      $("#filter-analyze-tab").before(
        `<a target="_blank" class="big-tab view-type-tab" id="xivanalysis-tab"><span class="zmdi zmdi-time-interval"></span> <span class="big-tab-text"><br>xivanalysis</span></a>`
      );
      $("#xivanalysis-tab").click(() => {
        $("#xivanalysis-tab").attr("href", `https://xivanalysis.com/report-redirect/${location.href}`);
      });

      $("#filter-type-tabs").css("cursor", "default");
      // add new tab 1 before last element
      $("#filter-type-tabs").find("a:nth-last-child(2)").after(`<a href="#" class="filter-type-tab drop" id="filter-lb-tab">LB</a>`);
      $("#filter-lb-tab").click(() => {
        changeHashParams({
          type: "summary",
          view: "events",
          pins: '2$Off$#ffff14$expression$type="limitbreakupdate"',
        });
        return false;
      });

      let jobs;
      const rankOnes = {};

      const onTableChange = () => {
        const hashParams = getHashParams();
        // Rankings Tab
        if (hashParams.view === "rankings") {
          if (!GM_getValue("apiKey")) return;
          const rows = [];
          if (!jobs) {
            fetch(`https://www.fflogs.com/v1/classes?api_key=${GM_getValue("apiKey")}`)
              .then((res) => res.json())
              .then((data) => {
                jobs = data[0].specs;
                rows.forEach((row) => {
                  updatePoints(row);
                });
              })
              .catch((err) => console.error(err));
          } else {
            setTimeout(() => {
              rows.forEach((row) => {
                updatePoints(row);
              });
            }, 0);
          }

          const updatePoints = async (row) => {
            const hashParams = getHashParams();
            const rank = Number($(row).find("td:nth-child(2)").text().replace("~", ""));
            const outOf = Number($(row).find("td:nth-child(3)").text().replace(",", ""));
            const dps = Number($(row).find("td:nth-child(6)").text().replace(",", ""));
            const jobName = $(row).find("td:nth-child(5) > a").attr("class") || "";
            const jobName2 = $(row).find("td:nth-child(5) > a:nth-last-child(1)").attr("class") || "";
            const playerMetric = hashParams.playermetric || "rdps";

            if (jobName2 !== "players-table-realm") {
              $(row)
                .find("td:nth-child(7)")
                .html(`<center><img src="https://cdn.7tv.app/emote/62523dbbbab59cfd1b8b889d/1x.webp" title="No api v1 endpoint for combined damage." style="height: 15px;"></center>`);
              return;
            }

            const updateCharecterAllStar = async () => {
              $(row).find("td:nth-child(7)").html(characterAllStar(rank, outOf, dps, rankOnes[jobName][playerMetric]).toFixed(2));
            };

            if (!rankOnes[jobName]) {
              rankOnes[jobName] = {};
            }

            if (!rankOnes[jobName][playerMetric]) {
              const url = `https://www.fflogs.com/v1/rankings/encounter/${reportsCache.filterFightBoss}?metric=${playerMetric}&spec=${
                jobs.find((job) => job.name.replace(" ", "") === jobName)?.id
              }&api_key=${GM_getValue("apiKey")}`;
              fetch(url)
                .then((res) => res.json())
                .then((data) => {
                  rankOnes[jobName][playerMetric] = Number(data.rankings[0].total.toFixed(1));
                  updateCharecterAllStar();
                })
                .catch((err) => console.error(err));
            } else {
              updateCharecterAllStar();
            }
          };

          $(".player-table").each((_i, table) => {
            $(table)
              .find("thead tr th:nth-child(6)")
              .after(
                `<th class="sorting ui-state-default" tabindex="0" aria-controls="DataTables_Table_0" rowspan="1" colspan="1" aria-label="Patch: activate to sort column ascending"><div class="DataTables_sort_wrapper">Points<span class="DataTables_sort_icon css_right ui-icon ui-icon-caret-2-n-s"></span></div></th>`
              );
            $(table)
              .find("tbody tr")
              .each((_i, row) => {
                $(row)
                  .find("td:nth-child(6)")
                  .after(`<td class="rank-per-second primary main-table-number"><center><span class="zmdi zmdi-spinner zmdi-hc-spin" style="color:white font-size:24px"></center></span></td>`);
                rows.push(row);
              });
          });
        }
        // Events Tab
        if (hashParams.view === "events") {
          $(".events-table")
            .find("thead tr th:nth-child(1)")
            .before(`<th class="ui-state-default sorting_disabled" rowspan="1" colspan="1"><div class="DataTables_sort_wrapper">Diff<span class="DataTables_sort_icon"></span></div></th>`);

          let last;
          $(".main-table-number").each((_i, cell) => {
            if (last) {
              const time = moment($(cell).text(), "m:ss.SSS");
              const diff = (time.diff(last) / 1000).toFixed(3);
              $(cell).before(`<td style="width: 2em; text-align: right;">${diff.padStart(5, "0")}</td>`);
              last = time;
            } else {
              $(cell).before(`<td style="width: 2em; text-align: right;"> - </td>`);
              last = moment($(cell).text(), "m:ss.SSS");
            }
          });

          // Limit Break
          if (hashParams.type === "summary" && hashParams.pins === `2$Off$#ffff14$expression$type="limitbreakupdate"`) {
            $(".filter-type-tab.selected").removeClass("selected");
            $("#filter-lb-tab").addClass("selected");

            $(".events-table")
              .find("thead tr th:nth-last-child(3)")
              .after(`<th class="ui-state-default sorting_disabled" rowspan="1" colspan="1"><div class="DataTables_sort_wrapper">Active<span class="DataTables_sort_icon"></span></div></th>`);
            $(".events-table")
              .find("thead tr th:nth-last-child(2)")
              .after(`<th class="ui-state-default sorting_disabled" rowspan="1" colspan="1"><div class="DataTables_sort_wrapper">Bars<span class="DataTables_sort_icon"></span></div></th>`);

            let last;
            $(".event-description-cell").each((_i, cell) => {
              const text = $(cell).text();
              if (text === "Event") {
                $(cell).html(`<div class="DataTables_sort_wrapper">Limit Break Total<span class="DataTables_sort_icon"></span></div>`);
                return;
              }

              // The limit break gauge updated to 30000. There are 3 total bars.
              const lb = text.match(/The limit break gauge updated to (\d+). There are (\d+) total bars./);
              const currentLb = Number(lb?.[1]);
              const currentBars = Number(lb?.[2]);

              if (lb) {
                let diff;
                if (last !== undefined) {
                  diff = (currentLb - last).toLocaleString();
                } else {
                  diff = " - ";
                }
                last = currentLb;
                let actualDiff = diff > 0 ? `+${diff}` : diff;

                if (
                  (currentBars === 3 && (diff === "220" || diff === "170" || diff === "160" || diff === "154" || diff === "144" || diff === "140")) ||
                  (currentBars === 2 && diff === "180") ||
                  (currentBars === 1 && diff === "75")
                ) {
                  diff = " - ";
                }

                $(cell).before(`<td style="width: 2em; text-align: right; white-space: nowrap;">${diff}</td>`);
                $(cell).html(`${Number(currentLb).toLocaleString()} / ${(Number(currentBars) * 10000).toLocaleString()} <span style="float: right;">${actualDiff}</span>`);
                $(cell).after(`<td style="width: 2em; text-align: right;">${currentBars}</td>`);
              }
            });
          } else {
            $("#filter-lb-tab").removeClass("selected");
            $(`#filter-${hashParams.type}-tab`).addClass("selected");
            if (hashParams.pins === `2$Off$#ffff14$expression$type="limitbreakupdate"`) {
              changeHashParams({ pins: "" });
            }
          }
        }
      };

      const tableContainer = document.querySelector("#table-container");
      if (tableContainer) {
        const observer = new MutationObserver(onTableChange);
        observer.observe(tableContainer, { attributes: true, characterData: true, childList: true });
      }
    }

    if (/\/zone\/rankings\/.+/.test(location.pathname)) {
    }

    if (/\/character\/.+/.test(location.pathname)) {
      $(".table-icon").removeAttr("alt");
      const jobList = $("#jobs-header-icons").children();
      const jobListSortedNumbers = [];
      const jobListSorted = [];

      JOB_ORDER.forEach((job) =>
        jobList
          .children()
          .toArray()
          .forEach((jobElement, i) => {
            if (jobElement.alt === job) jobListSortedNumbers.push(i);
          })
      );
      jobListSortedNumbers.forEach((i) => jobListSorted.push(jobList[i]));
      jobList.remove();
      $("#jobs-header-icons").append(jobListSorted);

      // Chad Bradly's Profile Customization
      if (/\/character\/id\/12781922/.test(location.pathname)) {
        $("#character-portrait-image").attr("src", "https://media.tenor.com/epNMHGvRyHcAAAAd/gigachad-chad.gif");
        $("#portrait-and-basics, #character-header-customize-action-box, #update-box").addClass("slightly-transparent-box");
        $("#character-portrait-box").css("background-image", 'url("https://i.imgur.com/dbwqHIt.png")').addClass("with-banner");
      }
    }

    if (/\/profile/.test(location.pathname)) {
      let apiKey = GM_getValue("apiKey");

      $("#api").after(`<div id="extension" class="dialog-block"></div>`);
      $("#extension").append(`<div id="extension-title" class="dialog-title">FF Progs</div>`);
      $("#extension").append(`<div id="extension-content" style="margin:1em"></div>`);

      const addApiInput = () => {
        $("#extension-content").append(`<div id="extension-contents" style="margin:1em">Enter your FFLogs API Key</div>`);
        $("#extension-content").append(`<input type=text id="apiKeyInput" style="margin-left: 10px">`);
        $("#extension-content").append(`<input type=button id="apiButton" style="margin-left: 10px" value="Save API Key">`);
        $("#apiButton").click(() => {
          apiKey = $("#apiKeyInput").val();
          GM_setValue("apiKey", apiKey);
          $("#apiButton").remove();
          $("#apiKeyInput").remove();
          $("#extension-content").append(`<div id="resloved-text" style="margin:1em">API Key Saved</div>`);
          setTimeout(() => {
            $("#extension-contents").remove();
            $("#resloved-text").remove();
            removeApiInput();
          }, 2000);
        });
      };

      const removeApiInput = () => {
        $("#extension-content").append(`<input type=button id="apiDeleteButton" style="margin-left: 10px" value="Remove API Key">`);
        $("#apiDeleteButton").click(() => {
          GM_deleteValue("apiKey");
          $("#apiDeleteButton").remove();
          $("#extension-content").append(`<div id="resloved-text" style="margin:1em">API Key Removed</div>`);
          setTimeout(() => {
            $("#resloved-text").remove();
            addApiInput();
          }, 2000);
        });
      };
      if (!apiKey) {
        addApiInput();
      } else {
        removeApiInput();
      }
    }
  }
})();