您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Improves FFlogs.
// ==UserScript== // @name FF Progs // @name:en FF Progs // @description Improves FFlogs. // @description:en Improves FFlogs. // @version 1.4.2 // @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/* // @match https://*.warcraftlogs.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 you want to add more filter expression presets, you can add them here. const PIN_PRESETS = { "No Filter": "", calulateddamage: "type='calculateddamage'", damage: "type='damage'", cast: "type='cast'", Targetability: "type='targetabilityupdate'", GCD: "ability.isOffGCD=false", "Kill Event": "kill", LB: `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};`, Magical: "ability.type=1024", Physical: "ability.type=128", Heal: "ability.type=8", DOT: "ability.type=64", "Buff/Debuff": "ability.type=1", Darkness: "ability.type=124", True: "ability.type=32", System: "ability.type=0", }; const ABILITY_TYPES = { 0: "System", 1: "Buff/Debuff", // 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 ]; 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 getQueryParams = () => { const queryParams = new URLSearchParams(window.location.search); const params = Object.fromEntries(queryParams.entries()); return params; }; const changeQueryParams = (defaultParams) => { const url = new URL(window.location); const queryParams = new URLSearchParams(url.search); Object.entries(defaultParams).forEach(([key, value]) => { if (value !== null && value !== undefined && value !== "") { queryParams.set(key, value); } else { queryParams.delete(key); } }); changeView(queryParams); }; const updateTable = (onTableChange) => { const tableContainer = document.querySelector("#table-container"); if (tableContainer) { const observer = new MutationObserver(onTableChange); observer.observe(tableContainer, { attributes: true, characterData: true, childList: true, }); } }; function parseTime(cell) { const [_, sign, m, s, ms] = $(cell) .text() .match(/(-?)(\d+):(\d+)\.(\d+)/); const totalMs = (+m * 60000 + +s * 1000 + +ms) * (sign === "-" ? -1 : 1); return totalMs; } 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, #gear-box-ad, .ad-placement-sticky-footer, .content-sidebar, #ap-ea8a4fe5-container, #tile-content-ap, #builds-banner" ).remove(); $(".content-with-sidebar").css("display", "block"); $("#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").on("click", () => { window.open(`https://xivanalysis.com/report-redirect/${location.href}`, "_blank"); }); if (!$("#filter-rankings-tab").length) { $("#filter-replay-tab").before( `<a href="#" class="big-tab view-type-tab" id="filter-rankings-tab" onclick="return changeFilterView('rankings', this)" oncontextmenu="changeFilterView('rankings', this)"><span class="zmdi zmdi-sort"></span><span class="big-tab-text"><br>Rankings</span></a>` ); } // Fixes Cursor behavior on Filter Type Tabs $("#filter-type-tabs").css("cursor", "default"); const rankOnes = {}; let jobs; const onTableChange = () => { let queryParams = getQueryParams(); let lastLbGain; let lastTimeDiff; let lastSkillName; let lastSelectedPreset; // Filter Presets if (!queryParams.view || queryParams.view === "events" || queryParams.view === "timeline" || queryParams.view === "execution") { if (!$("#presets").length) { $("#graph-title-strip > div:first-child").after( `<div style="margin-left: auto; padding-right: 8px;" id="presets"> No Overwrite: <input type="checkbox" id="no-overwrite" style="margin-right: 8px;"> Filter Presets: <select id="presets-select" style="margin-right: 8px;"> ${Object.entries(PIN_PRESETS) .map(([name, pin]) => `<option value="${pin}">${name}</option>`) .join("")} </select> </div>` ); $("#presets-select").on("change", (e) => { const selected = $("#presets-select").val(); const name = $("#presets-select option:selected").text(); if (!selected) { changeQueryParams({ type: "", view: "", source: "", hostility: "", pins: "", start: "", end: "", }); lastSelectedPreset = null; return; } if (name === "LB") { changeQueryParams({ type: "summary", view: "events", source: "", pins: PIN_PRESETS.LB, }); lastSelectedPreset = name return; } if (name === "Kill Event") { changeQueryParams({ type: "resources", view: "events", source: "", hostility: "1", pins: "", start: fightSegmentEndTime - 5 * 1000, end: fightSegmentEndTime, }); lastSelectedPreset = name return; } const pinTemplate = `2$Off$#244F4B$expression$${selected}`; if ($("#no-overwrite").is(":checked") && lastSelectedPreset !== "LB" && lastSelectedPreset !== "Kill Event") { queryParams = getQueryParams(); changeQueryParams({ pins: queryParams.pins ? `${queryParams.pins}^${pinTemplate}` : pinTemplate }); } else { changeQueryParams({ pins: pinTemplate }); } lastSelectedPreset = name }); } } // Rankings Tab if (queryParams.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 queryParams = getQueryParams(); 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 = queryParams.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 (queryParams.view === "events") { if (queryParams.type === "resources") { return; } $(".events-table") .find("thead tr th:nth-child(1)") .after(`<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 !== undefined) { const time = parseTime(cell); const diff = ((time - lastTimeDiff) / 1000).toFixed(3); const whiteListlastSkill = ["Ten", "Chi", "Jin", "Savage Claw"]; const whiteListSkill = ["Quadruple Technical Finish", "Pneuma", "Star Prism"]; let bgColor = ""; const skillName = $(cell).next().next().find("a").text(); if (queryParams.type === "casts" && !!queryParams.source && !whiteListlastSkill.includes(lastSkillName) && !!lastSkillName && !whiteListSkill.includes(skillName) && !!skillName) { if (diff < 0.576 && diff > 0.2) { bgColor = "background-color: chocolate !important;"; } if (diff > 5) { bgColor = "background-color: gray !important;"; } } lastSkillName = skillName; $(cell).after(`<td style="width: 2em; text-align: right; ${bgColor}">${diff}</td>`); lastTimeDiff = time; } else { $(cell).after(`<td style="width: 2em; text-align: right;"> - </td>`); lastTimeDiff = parseTime(cell); } }); if (queryParams.type === "casts" && queryParams.hostility === "1") { $(".event-ability-cell a").each((_i, cell) => { const actionId = $(cell).attr("href").split("/")[5]; const hexId = parseInt(actionId).toString(16); $(cell).text(`${$(cell).text()} [${hexId}]`); }); } } // LB Pin if (queryParams.view === "events" && queryParams.type === "summary" && queryParams.pins === PIN_PRESETS.LB) { $(".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>`); } }); } }; updateTable(onTableChange); } // Zone Rankings Page if (ZONE_RANKINGS_PATH_REGEX.test(location.pathname)) { const onTableChange = () => { // Auto Translate Report Links $(".main-table-name").each((_i, cell) => { if ($(cell).find(".main-table-guild").attr("href").includes("translate=true")) return; if ($(cell).find(".main-table-guild").attr("href").includes("guild")) return; $(cell) .find(".main-table-guild") .attr("href", `${$(cell).find(".main-table-guild").attr("href")}&translate=true`); }); }; onTableChange(); updateTable(onTableChange); } // 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); } })();