FF Progs

Improves FFlogs.

目前为 2023-07-21 提交的版本。查看 最新版本

// ==UserScript==
// @name                FF Progs
// @name:en             FF Progs
// @description         Improves FFlogs.
// @description:en      Improves FFlogs.
// @version             1.0.8
// @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";

  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 PASSIVE_LB_GAIN = [
    ["75"], // one bar
    ["180"], // two bars
    ["220", "170", "160", "154", "144", "140"], // three bars
  ];
  // this code was made in 1 day so its not the best but it works :D
  const LB_PIN = `2$Main$#ffff14$script$let l;pinMatchesFightEvent=(e,f)=>{switch(e.type){case"limitbreakupdate":return l&&l===e.timestamp||(l=e.timestamp),!0;case"calculateddamage":if("Player"===e.target.type&&e.timestamp===l)return!0;break;case"heal":if(!e.isTick&&e.timestamp===l)return!0}return!1};`;
  const REPORTS_PATH_REGEX = /\/reports\/.+/;
  const ZONE_RANKINGS_PATH_REGEX = /\/zone\/rankings\/.+/;
  const CHARACTER_PATH_REGEX = /\/character\/.+/;
  const PROFILE_PATH_REGEX = /\/profile/;
  const LB_REGEX =
    /The limit break gauge updated to (\d+). There are (\d+) total bars./;

  const apiKey = GM_getValue("apiKey");

  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) => {
    const hashParams = getHashParams();
    const newParams = {
      ...hashParams,
      ...defaultParams,
    };

    location.hash = Object.entries(newParams)
      .filter(
        ([_key, value]) =>
          !["undefined", "null", "", null, undefined].includes(value)
      )
      .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
    );
  };

  // AdBlock
  $(
    "#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");

  // Reports Page
  if (REPORTS_PATH_REGEX.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: LB_PIN,
      });
      return false;
    });

    let jobs;
    const rankOnes = {};

    const onTableChange = () => {
      const hashParams = getHashParams();
      let lastLbGain;
      let lastTimeDiff;
      // 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") {
        if (hashParams.type === "resources") {
          return;
        }
        $(".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>`
          );

        $(".main-table-number").each((_i, cell) => {
          if (lastTimeDiff) {
            const time = moment($(cell).text(), "m:ss.SSS");
            const diff = (time.diff(lastTimeDiff) / 1000).toFixed(3);
            let bgColor = "";
            if (hashParams.type === "casts" && hashParams.source) {
              if (diff < 0.575) {
                bgColor = "background-color: orange !important;";
              }
              if (diff < 0.535) {
                bgColor = "background-color: chocolate !important;";
              }
              if (diff < 0.475) {
                bgColor = "background-color: red !important;";
              }
              if (diff < 0.435) {
                bgColor = "background-color: purple !important;";
              }
            }
            $(cell).before(
              `<td style="width: 2em; text-align: right; ${bgColor}">${diff.padStart(
                5,
                "0"
              )}</td>`
            );
            lastTimeDiff = time;
          } else {
            $(cell).before(
              `<td style="width: 2em; text-align: right;"> - </td>`
            );
            lastTimeDiff = moment($(cell).text(), "m:ss.SSS");
          }
        });

        if (hashParams.type === "casts" && hashParams.hostility === "1") {
          $(".event-ability-cell a").each((_i, cell) => {
            const actionId = $(cell).attr("href").split("/")[5];
            console.log(actionId);
            const hexId = parseInt(actionId).toString(16);
            $(cell).text(`${$(cell).text()} [${hexId}]`);

          });
        }
      }

      // LB Tab
      if (
        hashParams.view === "events" &&
        hashParams.type === "summary" &&
        hashParams.pins === LB_PIN
      ) {
        $(".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>`
          );

        $(".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;
          }

          if (!LB_REGEX.test(text)) {
            $(cell).before(
              `<td style="width: 2em; text-align: right; white-space: nowrap;"> * </td>`
            );
            $(cell).after(
              `<td style="width: 2em; text-align: right;"> * </td>`
            );
            return;
          }

          const lb = text.match(LB_REGEX);
          const currentLb = Number(lb?.[1]);
          const currentBars = Number(lb?.[2]);

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

            if (PASSIVE_LB_GAIN[currentBars - 1].includes(diff)) {
              // passive lb gain
              diff = " - ";
            } else {
              // active lb gain
            }

            $(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 if (hashParams.pins === LB_PIN) {
        $("#filter-lb-tab").removeClass("selected");
        $(`#filter-${hashParams.type}-tab`).addClass("selected");
        changeHashParams({ pins: "" });
      }
    };

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

  // Zone Rankings Page
  if (ZONE_RANKINGS_PATH_REGEX.test(location.pathname)) {
    const onTableChange = () => {
      $(".main-table-name").each((_i, cell) => {
        if ($(cell).find(".main-table-realm").text().includes("(JP)")) {
          if (
            $(cell)
              .find(".main-table-guild")
              .attr("href")
              .includes("translate=true")
          )
            return;
          $(cell)
            .find(".main-table-guild")
            .attr(
              "href",
              `${$(cell).find(".main-table-guild").attr("href")}&translate=true`
            );
        }
      });
    };

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

  // Character Page
  if (CHARACTER_PATH_REGEX.test(location.pathname)) {
    // Chad Bradly's Profile Customization
    const CHAD_ID_REGEX = /\/character\/id\/12781922/;
    const CHAD_NAME_REGEX = /\/character\/na\/sargatanas\/chad%20bradly/;
    const CHAD_ICON_URL =
      "https://media.tenor.com/epNMHGvRyHcAAAAd/gigachad-chad.gif";

    if (
      CHAD_ID_REGEX.test(location.pathname) ||
      CHAD_NAME_REGEX.test(location.pathname)
    ) {
      $("#character-portrait-image").attr("src", CHAD_ICON_URL);
    }
  }

  // Profile Page
  if (PROFILE_PATH_REGEX.test(location.pathname)) {
    const $extension = $(`
      <div id="extension" class="dialog-block">
        <div id="extension-title" class="dialog-title">FF Progs</div>
        <div id="extension-content" style="margin:1em"></div>
      </div>
    `);

    const $apiInputContainer = $(`
      <div id="api-input-container" style="margin:1em">
        <div>Enter your FFLogs API Key</div>
        <input type="text" id="api-key-input" style="margin-left: 10px" value="${
          apiKey || ""
        }">
        <input type="button" id="api-save-button" style="margin-left: 10px" value="${
          apiKey ? "Update API Key" : "Save API Key"
        }">
      </div>
    `);

    const $apiStatus = $(`
      <div id="api-status" style="margin:1em; display: ${
        apiKey ? "block" : "none"
      }">
        <div>API Key ${apiKey ? "saved" : "not saved"}</div>
        <input type="button" id="api-remove-button" style="margin-left: 10px" value="Remove API Key">
      </div>
    `);

    const saveApiKey = () => {
      const newApiKey = $("#api-key-input").val().trim();
      if (newApiKey) {
        GM_setValue("apiKey", newApiKey);
        $apiStatus.show().find("div").text("API Key saved");
        $apiInputContainer.hide();
        setTimeout(() => {
          $apiStatus.hide();
          $apiInputContainer.show();
        }, 2000);
      }
    };

    const removeApiKey = () => {
      GM_deleteValue("apiKey");
      $apiStatus.show().find("div").text("API Key removed");
      $apiStatus.find("#api-remove-button").remove();
      $apiInputContainer.show();
      setTimeout(() => {
        $apiStatus.hide();
      }, 2000);
    };

    $extension.insertAfter("#api");
    $apiInputContainer.appendTo("#extension-content");
    $apiStatus.appendTo("#extension-content");

    $apiInputContainer.on("click", "#api-save-button", saveApiKey);
    $apiStatus.on("click", "#api-remove-button", removeApiKey);
  }
})();