FF Progs

Improves FFlogs.

当前为 2024-03-07 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name FF Progs
  3. // @name:en FF Progs
  4. // @description Improves FFlogs.
  5. // @description:en Improves FFlogs.
  6. // @version 1.0.9
  7. // @namespace k_fizzel
  8. // @author Chad Bradly
  9. // @website https://www.fflogs.com/character/id/12781922
  10. // @icon https://assets.rpglogs.com/img/ff/favicon.png?v=2
  11. // @match https://*.fflogs.com/*
  12. // @require https://code.jquery.com/jquery-3.2.0.min.js
  13. // @grant unsafeWindow
  14. // @grant GM_addStyle
  15. // @grant GM_getValue
  16. // @grant GM_setValue
  17. // @grant GM_deleteValue
  18. // @license MIT License
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. "use strict";
  23.  
  24. const JOB_ORDER = [
  25. // Tanks
  26. "Paladin",
  27. "Warrior",
  28. "DarkKnight",
  29. "Gunbreaker",
  30. // Healers
  31. "WhiteMage",
  32. "Scholar",
  33. "Astrologian",
  34. "Sage",
  35. // Melee
  36. "Monk",
  37. "Dragoon",
  38. "Ninja",
  39. "Samurai",
  40. "Reaper",
  41. // Physical Ranged
  42. "Bard",
  43. "Machinist",
  44. "Dancer",
  45. // Magical Ranged
  46. "BlackMage",
  47. "Summoner",
  48. "RedMage",
  49. ];
  50. const ABILITY_TYPES = {
  51. 0: "None",
  52. 1: "Buff",
  53. 2: "Unknown",
  54. 4: "Unknown",
  55. 8: "Heal",
  56. 16: "Unknown",
  57. 32: "True",
  58. 64: "DOT",
  59. 124: "Darkness",
  60. 125: "Darkness",
  61. 126: "Darkness",
  62. 127: "Darkness",
  63. 128: "Physical",
  64. 256: "Magical",
  65. 512: "Unknown",
  66. 1024: "Magical",
  67. };
  68. const PASSIVE_LB_GAIN = [
  69. ["75"], // one bar
  70. ["180"], // two bars
  71. ["220", "170", "160", "154", "144", "140"], // three bars
  72. ];
  73. // this code was made in 1 day so its not the best but it works :D
  74. 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};`;
  75. const REPORTS_PATH_REGEX = /\/reports\/.+/;
  76. const ZONE_RANKINGS_PATH_REGEX = /\/zone\/rankings\/.+/;
  77. const CHARACTER_PATH_REGEX = /\/character\/.+/;
  78. const PROFILE_PATH_REGEX = /\/profile/;
  79. const LB_REGEX = /The limit break gauge updated to (\d+). There are (\d+) total bars./;
  80.  
  81. const apiKey = GM_getValue("apiKey");
  82.  
  83. const getHashParams = () => {
  84. const hash = window.location.hash.substring(1);
  85. const params = {};
  86.  
  87. hash.split("&").forEach((pair) => {
  88. const [key, value] = pair.split("=");
  89. params[key] = decodeURIComponent(value);
  90. });
  91.  
  92. return params;
  93. };
  94.  
  95. const changeHashParams = (defaultParams) => {
  96. const hashParams = getHashParams();
  97. const newParams = {
  98. ...hashParams,
  99. ...defaultParams,
  100. };
  101.  
  102. location.hash = Object.entries(newParams)
  103. .filter(([_key, value]) => !["undefined", "null", "", null, undefined].includes(value))
  104. .map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
  105. .join("&");
  106. };
  107.  
  108. const characterAllStar = (rank, outOf, rDPS, rankOneRDPS) => {
  109. return Math.min(Math.max(100 * (rDPS / rankOneRDPS), 100 - (rank / outOf) * 100) + 20 * (rDPS / rankOneRDPS), 120);
  110. };
  111.  
  112. // AdBlock
  113. $("#top-banner, .side-rail-ads, #bottom-banner, #subscription-message-tile-container, #playwire-video-container, #right-ad-box, #right-vertical-banner, #gear-box-ad, .ad-placement-sticky-footer, .content-sidebar").remove();
  114. $(".content-with-sidebar").css("display", "block");
  115. $("#table-container").css("margin", "0 0 0 0");
  116.  
  117. // Reports Page
  118. if (REPORTS_PATH_REGEX.test(location.pathname)) {
  119. // Add XIV Analysis Button
  120. $("#filter-analyze-tab").before(
  121. `<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>`
  122. );
  123. $("#xivanalysis-tab").click(() => {
  124. $("#xivanalysis-tab").attr("href", `https://xivanalysis.com/report-redirect/${location.href}`);
  125. });
  126.  
  127. $("#filter-type-tabs").css("cursor", "default");
  128. // add new tab 1 before last element
  129. $("#filter-type-tabs").find("a:nth-last-child(2)").after(`<a href="#" class="filter-type-tab drop" id="filter-lb-tab">LB</a>`);
  130. $("#filter-lb-tab").click(() => {
  131. changeHashParams({
  132. type: "summary",
  133. view: "events",
  134. pins: LB_PIN,
  135. });
  136. return false;
  137. });
  138.  
  139. let jobs;
  140. const rankOnes = {};
  141.  
  142. const onTableChange = () => {
  143. const hashParams = getHashParams();
  144. let lastLbGain;
  145. let lastTimeDiff;
  146. // Rankings Tab
  147. if (hashParams.view === "rankings") {
  148. if (!GM_getValue("apiKey")) return;
  149. const rows = [];
  150. if (!jobs) {
  151. fetch(`https://www.fflogs.com/v1/classes?api_key=${GM_getValue("apiKey")}`)
  152. .then((res) => res.json())
  153. .then((data) => {
  154. jobs = data[0].specs;
  155. rows.forEach((row) => {
  156. updatePoints(row);
  157. });
  158. })
  159. .catch((err) => console.error(err));
  160. } else {
  161. setTimeout(() => {
  162. rows.forEach((row) => {
  163. updatePoints(row);
  164. });
  165. }, 0);
  166. }
  167.  
  168. const updatePoints = async (row) => {
  169. const hashParams = getHashParams();
  170. const rank = Number($(row).find("td:nth-child(2)").text().replace("~", ""));
  171. const outOf = Number($(row).find("td:nth-child(3)").text().replace(",", ""));
  172. const dps = Number($(row).find("td:nth-child(6)").text().replace(",", ""));
  173. const jobName = $(row).find("td:nth-child(5) > a").attr("class") || "";
  174. const jobName2 = $(row).find("td:nth-child(5) > a:nth-last-child(1)").attr("class") || "";
  175. const playerMetric = hashParams.playermetric || "rdps";
  176.  
  177. if (jobName2 !== "players-table-realm") {
  178. $(row)
  179. .find("td:nth-child(7)")
  180. .html(`<center><img src="https://cdn.7tv.app/emote/62523dbbbab59cfd1b8b889d/1x.webp" title="No api v1 endpoint for combined damage." style="height: 15px;"></center>`);
  181. return;
  182. }
  183.  
  184. const updateCharecterAllStar = async () => {
  185. $(row).find("td:nth-child(7)").html(characterAllStar(rank, outOf, dps, rankOnes[jobName][playerMetric]).toFixed(2));
  186. };
  187.  
  188. if (!rankOnes[jobName]) {
  189. rankOnes[jobName] = {};
  190. }
  191.  
  192. if (!rankOnes[jobName][playerMetric]) {
  193. const url = `https://www.fflogs.com/v1/rankings/encounter/${reportsCache.filterFightBoss}?metric=${playerMetric}&spec=${
  194. jobs.find((job) => job.name.replace(" ", "") === jobName)?.id
  195. }&api_key=${GM_getValue("apiKey")}`;
  196. fetch(url)
  197. .then((res) => res.json())
  198. .then((data) => {
  199. rankOnes[jobName][playerMetric] = Number(data.rankings[0].total.toFixed(1));
  200. updateCharecterAllStar();
  201. })
  202. .catch((err) => console.error(err));
  203. } else {
  204. updateCharecterAllStar();
  205. }
  206. };
  207.  
  208. $(".player-table").each((_i, table) => {
  209. $(table)
  210. .find("thead tr th:nth-child(6)")
  211. .after(
  212. `<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>`
  213. );
  214. $(table)
  215. .find("tbody tr")
  216. .each((_i, row) => {
  217. $(row)
  218. .find("td:nth-child(6)")
  219. .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>`);
  220. rows.push(row);
  221. });
  222. });
  223. }
  224.  
  225. // Events Tab
  226. if (hashParams.view === "events") {
  227. if (hashParams.type === "resources") {
  228. return;
  229. }
  230. $(".events-table")
  231. .find("thead tr th:nth-child(1)")
  232. .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>`);
  233.  
  234. $(".main-table-number").each((_i, cell) => {
  235. if (lastTimeDiff) {
  236. const time = moment($(cell).text(), "m:ss.SSS");
  237. const diff = (time.diff(lastTimeDiff) / 1000).toFixed(3);
  238. let bgColor = "";
  239. if (hashParams.type === "casts" && hashParams.source) {
  240. if (diff < 0.575) {
  241. bgColor = "background-color: orange !important;";
  242. }
  243. if (diff < 0.535) {
  244. bgColor = "background-color: chocolate !important;";
  245. }
  246. if (diff < 0.475) {
  247. bgColor = "background-color: red !important;";
  248. }
  249. if (diff < 0.435) {
  250. bgColor = "background-color: purple !important;";
  251. }
  252. }
  253. $(cell).before(`<td style="width: 2em; text-align: right; ${bgColor}">${diff.padStart(5, "0")}</td>`);
  254. lastTimeDiff = time;
  255. } else {
  256. $(cell).before(`<td style="width: 2em; text-align: right;"> - </td>`);
  257. lastTimeDiff = moment($(cell).text(), "m:ss.SSS");
  258. }
  259. });
  260.  
  261. if (hashParams.type === "casts" && hashParams.hostility === "1") {
  262. $(".event-ability-cell a").each((_i, cell) => {
  263. const actionId = $(cell).attr("href").split("/")[5];
  264. console.log(actionId);
  265. const hexId = parseInt(actionId).toString(16);
  266. $(cell).text(`${$(cell).text()} [${hexId}]`);
  267. });
  268. }
  269. }
  270.  
  271. // LB Tab
  272. if (hashParams.view === "events" && hashParams.type === "summary" && hashParams.pins === LB_PIN) {
  273. $(".filter-type-tab.selected").removeClass("selected");
  274. $("#filter-lb-tab").addClass("selected");
  275.  
  276. $(".events-table")
  277. .find("thead tr th:nth-last-child(3)")
  278. .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>`);
  279. $(".events-table")
  280. .find("thead tr th:nth-last-child(2)")
  281. .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>`);
  282.  
  283. $(".event-description-cell").each((_i, cell) => {
  284. const text = $(cell).text();
  285. if (text === "Event") {
  286. $(cell).html(`<div class="DataTables_sort_wrapper">Limit Break Total<span class="DataTables_sort_icon"></span></div>`);
  287. return;
  288. }
  289.  
  290. if (!LB_REGEX.test(text)) {
  291. $(cell).before(`<td style="width: 2em; text-align: right; white-space: nowrap;"> * </td>`);
  292. $(cell).after(`<td style="width: 2em; text-align: right;"> * </td>`);
  293. return;
  294. }
  295.  
  296. const lb = text.match(LB_REGEX);
  297. const currentLb = Number(lb?.[1]);
  298. const currentBars = Number(lb?.[2]);
  299.  
  300. if (lb) {
  301. let diff;
  302. if (lastLbGain !== undefined) {
  303. diff = (currentLb - lastLbGain).toLocaleString();
  304. } else {
  305. diff = " - ";
  306. }
  307. lastLbGain = currentLb;
  308. let actualDiff = diff > 0 ? `+${diff}` : diff;
  309.  
  310. if (PASSIVE_LB_GAIN[currentBars - 1].includes(diff)) {
  311. // passive lb gain
  312. diff = " - ";
  313. } else {
  314. // active lb gain
  315. }
  316.  
  317. $(cell).before(`<td style="width: 2em; text-align: right; white-space: nowrap;">${diff}</td>`);
  318. $(cell).html(`${Number(currentLb).toLocaleString()} / ${(Number(currentBars) * 10000).toLocaleString()} <span style="float: right;">${actualDiff}</span>`);
  319. $(cell).after(`<td style="width: 2em; text-align: right;">${currentBars}</td>`);
  320. }
  321. });
  322. } else if (hashParams.pins === LB_PIN) {
  323. $("#filter-lb-tab").removeClass("selected");
  324. $(`#filter-${hashParams.type}-tab`).addClass("selected");
  325. changeHashParams({ pins: "" });
  326. }
  327. };
  328.  
  329. const tableContainer = document.querySelector("#table-container");
  330. if (tableContainer) {
  331. const observer = new MutationObserver(onTableChange);
  332. observer.observe(tableContainer, {
  333. attributes: true,
  334. characterData: true,
  335. childList: true,
  336. });
  337. }
  338. }
  339.  
  340. // Zone Rankings Page
  341. if (ZONE_RANKINGS_PATH_REGEX.test(location.pathname)) {
  342. const onTableChange = () => {
  343. $(".main-table-name").each((_i, cell) => {
  344. if ($(cell).find(".main-table-realm").text().includes("(JP)")) {
  345. if ($(cell).find(".main-table-guild").attr("href").includes("translate=true")) return;
  346. $(cell)
  347. .find(".main-table-guild")
  348. .attr("href", `${$(cell).find(".main-table-guild").attr("href")}&translate=true`);
  349. }
  350. });
  351. };
  352.  
  353. onTableChange();
  354. const tableContainer = document.querySelector("#table-container");
  355. if (tableContainer) {
  356. const observer = new MutationObserver(onTableChange);
  357. observer.observe(tableContainer, {
  358. attributes: true,
  359. characterData: true,
  360. childList: true,
  361. });
  362. }
  363. }
  364.  
  365. // Character Page
  366. if (CHARACTER_PATH_REGEX.test(location.pathname)) {
  367. // Chad Bradly's Profile Customization
  368. const CHAD_ID_REGEX = /\/character\/id\/12781922/;
  369. const CHAD_NAME_REGEX = /\/character\/na\/sargatanas\/chad%20bradly/;
  370. const CHAD_ICON_URL = "https://media.tenor.com/epNMHGvRyHcAAAAd/gigachad-chad.gif";
  371.  
  372. if (CHAD_ID_REGEX.test(location.pathname) || CHAD_NAME_REGEX.test(location.pathname)) {
  373. $("#character-portrait-image").attr("src", CHAD_ICON_URL);
  374. }
  375. }
  376.  
  377. // Profile Page
  378. if (PROFILE_PATH_REGEX.test(location.pathname)) {
  379. const $extension = $(`
  380. <div id="extension" class="dialog-block">
  381. <div id="extension-title" class="dialog-title">FF Progs</div>
  382. <div id="extension-content" style="margin:1em"></div>
  383. </div>
  384. `);
  385.  
  386. const $apiInputContainer = $(`
  387. <div id="api-input-container" style="margin:1em">
  388. <div>Enter your FFLogs API Key</div>
  389. <input type="text" id="api-key-input" style="margin-left: 10px" value="${apiKey || ""}">
  390. <input type="button" id="api-save-button" style="margin-left: 10px" value="${apiKey ? "Update API Key" : "Save API Key"}">
  391. </div>
  392. `);
  393.  
  394. const $apiStatus = $(`
  395. <div id="api-status" style="margin:1em; display: ${apiKey ? "block" : "none"}">
  396. <div>API Key ${apiKey ? "saved" : "not saved"}</div>
  397. <input type="button" id="api-remove-button" style="margin-left: 10px" value="Remove API Key">
  398. </div>
  399. `);
  400.  
  401. const saveApiKey = () => {
  402. const newApiKey = $("#api-key-input").val().trim();
  403. if (newApiKey) {
  404. GM_setValue("apiKey", newApiKey);
  405. $apiStatus.show().find("div").text("API Key saved");
  406. $apiInputContainer.hide();
  407. setTimeout(() => {
  408. $apiStatus.hide();
  409. $apiInputContainer.show();
  410. }, 2000);
  411. }
  412. };
  413.  
  414. const removeApiKey = () => {
  415. GM_deleteValue("apiKey");
  416. $apiStatus.show().find("div").text("API Key removed");
  417. $apiStatus.find("#api-remove-button").remove();
  418. $apiInputContainer.show();
  419. setTimeout(() => {
  420. $apiStatus.hide();
  421. }, 2000);
  422. };
  423.  
  424. $extension.insertAfter("#api");
  425. $apiInputContainer.appendTo("#extension-content");
  426. $apiStatus.appendTo("#extension-content");
  427.  
  428. $apiInputContainer.on("click", "#api-save-button", saveApiKey);
  429. $apiStatus.on("click", "#api-remove-button", removeApiKey);
  430. }
  431. })();