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