FF Progs

Improves FFlogs.

Устаревшая версия за 11.05.2023. Перейдите к последней версии.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

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

  const getCDPS = (rDPS, aDPS, nDPS, startMS, endMS) => {
    const cdam = rDPS + aDPS - nDPS;
    const time = (endMS - startMS) / 1000;
    return cdam / time;
  };

  // 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 updateCDPS = () => {
          const REPORT_TABLE_URL = `https://www.fflogs.com:443/v1/report/tables/damage-done/LxC6PD3Tq8Mhvngy?start=${filterFightStartTime}&end=${filterFightEndTime}&api_key=${GM_getValue("apiKey")}`;
          $(".player-table").each((_i, table) => {
            $(table).find("thead tr th:nth-child(6)").html(`<div class="DataTables_sort_wrapper">cDPS<span class="DataTables_sort_icon css_right ui-icon ui-icon-caret-2-n-s"></span></div>`);
            $(table)
              .find("tbody tr")
              .each((_i, row) => {
                $(row)
                  .find("td:nth-child(6)")
                  .removeClass("rdps primary ndps")
                  .addClass("adps")
                  .html(`<center><span class="zmdi zmdi-spinner zmdi-hc-spin" style="color:white font-size:24px"></center></span>`);
              });
          });
          fetch(REPORT_TABLE_URL)
            .then((res) => res.json())
            .then((data) => {
              const players = data.entries;
              players.forEach((player) => {
                const playerName = player.name;
                const cDPS = getCDPS(player.totalRDPS, player.totalADPS, player.totalNDPS, filterFightStartTime, filterFightEndTime);
                $(".main-table-name a").each((_i, name) => {
                  // check if name parent has 2 imag tags in it
                  if ($(name).parent().find("img").length === 2) {
                    $(name)
                      .parent()
                      .parent()
                      .find("td:nth-child(6)")
                      .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;
                  }

                  if ($(name).text() === playerName) {
                    $(name)
                      .parent()
                      .parent()
                      .find("td:nth-child(6)")
                      .text(Number(cDPS.toFixed(1)).toLocaleString());
                  }
                });
              });
            });
        };

        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);
            });
        });

        // get the last .report-rankings-tab and add a button to view cdps
        $(".report-rankings-tab").last().after(`<div class="report-rankings-tab" id="report-rankings-tab-bosscdps">Damage (cDPS)</div>`);
        $("#report-rankings-tab-bosscdps").on("click", () => {
          updateCDPS();
        });
      }

      // 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>`);

        $(".main-table-number").each((_i, cell) => {
          if (lastTimeDiff) {
            const time = moment($(cell).text(), "m:ss.SSS");
            const diff = (time.diff(lastTimeDiff) / 1000).toFixed(3);
            $(cell).before(`<td style="width: 2em; text-align: right;">${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");
          }
        });
      }

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