Show CPR data, add status icon to unavailable, highlight member inefficiency
// ==UserScript==
// @name OC Track CPR
// @namespace heartflower.torn
// @version 1.1.2
// @description Show CPR data, add status icon to unavailable, highlight member inefficiency
// @author Heartflower
// @match https://www.torn.com/factions.php?*
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant GM.xmlHttpRequest
// @grant GM_registerMenuCommand
// ==/UserScript==
(function () {
"use strict";
console.log("[HF] OC Track CPR running");
// API SETTINGS //
let apiKey;
const storedAPIKey = localStorage.getItem("hf-tornstats-apiKey");
if (storedAPIKey) {
apiKey = storedAPIKey;
if (typeof GM_registerMenuCommand === "function")
GM_registerMenuCommand("Remove API key", removeAPIKey);
} else {
setAPIkey();
}
const pda = "xmlhttpRequest" in GM;
const httpRequest = pda ? "xmlhttpRequest" : "xmlHttpRequest";
function getLocalStorageJSON(key, defaultValue = {}) {
try {
return JSON.parse(localStorage.getItem(key)) ?? defaultValue;
} catch {
console.warn(`[HF] Failed to parse ${key}`);
return defaultValue;
}
}
let settings = getLocalStorageJSON("hf-oc-cpr-settings");
let tornStatsData = getLocalStorageJSON("hf-cached-ts-oc-data");
let localData = getLocalStorageJSON("hf-cached-local-oc-data");
let crimeLevelData = getLocalStorageJSON("hf-oc-level-data");
let weightData = getLocalStorageJSON("hf-cached-oc-weight-data");
const difficultyTiers = {
1: "introductory",
2: "simple",
3: "intermediate",
4: "advanced",
5: "elaborate",
};
function setAPIkey() {
const enterAPIKey = prompt(
"Enter the API key you used to create TornStats here:",
);
if (enterAPIKey !== null && enterAPIKey.trim() !== "") {
localStorage.setItem("hf-tornstats-apiKey", enterAPIKey);
alert("API key set succesfully");
apiKey = enterAPIKey;
if (typeof GM_registerMenuCommand === "function")
GM_registerMenuCommand("Remove API key", removeAPIKey);
} else {
alert("No valid API key entered!");
if (typeof GM_registerMenuCommand === "function")
GM_registerMenuCommand("Set API key", setAPIkey);
}
}
function removeAPIKey() {
const wantToDelete = confirm(
"Are you sure you want to remove your API key?",
);
if (wantToDelete) {
localStorage.removeItem("hf-tornstats-apiKey");
alert("API key successfully removed.");
} else {
alert("API key not removed.");
}
}
// REST OF THE SCRIPT //
function hookFetch(target) {
if (!target || !target.fetch) return;
const originalFetch = target.fetch;
target.fetch = function (...args) {
return originalFetch.apply(this, args).then(async (response) => {
const cloned = response.clone();
let text;
try {
text = await cloned.text();
} catch (e) {
text = "[Could not read response]";
}
const url = args[0];
if (!url) return response;
// If url is a Request object
if (url instanceof Request) url = url.url;
if (url.includes("usersNotInvolved")) {
try {
listenRecruitBtn(JSON.parse(text));
} catch (err) {
console.error("[HF] Failed to parse usersNotInvolved:", err);
}
} else if (url.includes("crimeList")) {
try {
crimeList(JSON.parse(text));
} catch (err) {
console.error("[HF] Failed to parse crimeList:", err);
}
}
return response; // return original so site still works
});
};
}
async function handleUninvoled(data, uninvolvedEls, lists) {
const statuses = await fetchMembers();
for (const user of data.users) {
const userId = user.userID;
const status = statuses[userId];
for (const element of uninvolvedEls) {
const elementUserId = Number(
element.href
.replace("https://www.torn.com", "")
.replace("/profiles.php?XID=", ""),
);
if (elementUserId === userId) {
const username = element.textContent;
const existingIcon =
element.parentNode.querySelector(".hf-activity-icon");
if (existingIcon) break;
const icon = document.createElement("div");
icon.classList.add("hf-activity-icon");
if (status === "Online") {
icon.style.backgroundPosition = "0 0";
} else if (status === "Idle") {
icon.style.backgroundPosition = "-1098px 0";
} else if (status === "Offline") {
icon.style.backgroundPosition = "-18px 0";
}
icon.addEventListener("click", function () {
createCPRmodal(username, userId);
});
element.parentNode.style.display = "flex";
element.parentNode.style.alignItems = "center";
element.parentNode.prepend(icon);
element.parentNode.parentNode.style.gridTemplateColumns =
"repeat(auto-fill, minmax(125px, 1fr))";
break;
}
}
}
for (const list of lists) {
const a = list.querySelector("a");
if (!a) list.style.display = "none";
}
}
async function listenRecruitBtn(data) {
const recruitBtn = document.body.querySelector(
`#faction-crimes-root [class*="buttonsContainer__"] [class*="button__"]`,
);
if (recruitBtn.className.includes("active__")) {
findCrimeRoot(data);
} else {
recruitBtn.addEventListener("click", function () {
findCrimeRoot(data);
});
}
}
async function crimeList(data) {
const loggedInUserId = JSON.parse(
document.body.querySelector("#torn-user")?.value,
)?.id;
let members = {};
const existingMembers = localStorage.getItem("hf-cached-local-oc-data");
if (existingMembers) {
try {
members = JSON.parse(existingMembers);
} catch (e) {
console.warn("[HF] Failed to parse existing members data");
members = {};
}
}
for (const crime of data.data) {
const crimeName = crime.scenario.name;
let roles = [];
let cpr = {};
const slots = crime.playerSlots;
for (const slot of slots) {
// const position = slot.name.replace(/ #\d+$/, "");
const position = slot.name;
if (!roles.includes(position)) roles.push(position);
const userId = slot.player?.ID;
const targetId = userId || loggedInUserId;
const m = (members[targetId] ??= {});
const c = (m[crimeName] ??= {});
const next = Number(slot.successChance);
const prev = Number(c[position] ?? -1);
if (next > prev) c[position] = slot.successChance;
if (userId) cpr[userId] = slot.successChance;
}
if (crimeName && crime.scenario.level && crime.scenario.difficultyTier) {
crimeLevelData[crimeName] = {
level: crime.scenario.level,
difficulty: difficultyTiers[crime.scenario.difficultyTier],
roles: roles,
};
}
const crimeId = crime.ID;
const crimeEl = await findOC(crimeId);
const slotEls = crimeEl?.querySelectorAll(`[class*="slotBody__"]`);
if (!slotEls || slotEls.length < 2) continue;
for (const slotEl of slotEls) {
const wrapper = slotEl.parentNode;
if (wrapper.className.includes("waitingJoin")) continue;
const crimeName = wrapper.parentNode.parentNode.querySelector(
`[class*="panelTitle__"]`,
)?.textContent;
const role = wrapper.querySelector(`[class*="title__"]`)?.textContent;
// .replace(/ #\d+$/, "");
const a = slotEl.querySelector(`[class*="slotMenuItem__"]`);
if (!a) continue;
const userId = Number(
a.href
.replace("https://www.torn.com", "")
.replace("/profiles.php?XID=", ""),
);
highlightCPR(cpr, userId, slotEl, crimeName, role);
showCPRinfo(a.parentNode, userId);
}
}
localData = members;
localStorage.setItem("hf-cached-local-oc-data", JSON.stringify(localData));
localStorage.setItem("hf-oc-level-data", JSON.stringify(crimeLevelData));
}
async function highlightCPR(cpr, userId, slotEl, crimeName, role) {
let unavailable = false;
const slotIcon = slotEl.parentNode.querySelector(`[class*="slotIcon__"]`);
const svg = slotIcon?.querySelector("svg");
const path = svg?.querySelector("path");
if (path?.getAttribute("fill") === "#ff794c") unavailable = true;
const active =
document.body.querySelector(`[class*="active__"]`).textContent;
if (active === "Completed") unavailable = false;
const slotHeader = slotEl.parentNode.querySelector(
`[class*="slotHeader__"]`,
);
function normalizeName(str) {
return str
.trim()
.split(/\s+/)
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join("");
}
const weight =
weightData[normalizeName(crimeName)]?.[
role?.replace(/[^a-zA-Z0-9]/g, "")
] ?? 1;
function getMinCPR(map, weight) {
if (!map) return 65;
const w = Number(weight);
return (
Object.entries(map)
.map(([k, v]) => [Number(k), Number(v)])
.filter(([k]) => k <= w)
.sort((a, b) => b[0] - a[0])[0]?.[1] ?? 65
);
}
const minCPR =
settings.weight === "true"
? getMinCPR(settings[crimeName]?.weight, weight)
: (settings[crimeName]?.[role] ?? 65);
if (settings.highlight === "true" && unavailable === true) {
slotEl.style.background = "var(--default-bg-18-gradient)"; // Yellow
slotHeader.style.background = "var(--default-bg-18-gradient)"; // Yellow
} else if (
settings.highlight === "true" &&
Number(cpr[userId]) < Number(minCPR)
) {
slotEl.style.background = "var(--default-bg-17-gradient)"; // Red
slotHeader.style.background = "var(--default-bg-17-gradient)"; // Red
}
}
async function showCPRinfo(slotMenu, userId) {
const existingBtn = slotMenu.querySelector(".hf-cpr-data-btn");
if (existingBtn) return;
const span = document.createElement("span");
span.textContent = "CPR Data";
span.classList.add("hf-cpr-data-btn");
slotMenu.prepend(span);
const username =
slotMenu.parentNode?.querySelector(`[class*="badge__"]`)?.textContent;
if (!username) return;
span.addEventListener("click", function () {
createCPRmodal(username, userId);
});
}
function fetchTornStatsData() {
const apiUrl = `https://www.tornstats.com/api/v2/${apiKey}/faction/cpr`;
GM[httpRequest]({
method: "GET",
url: apiUrl,
responseType: "json",
onload: function (response) {
try {
response.response ??= JSON.parse(response.responseText); // In order for it to work with Torn PDA
const data = response.response;
tornStatsData = data.members;
localStorage.setItem(
"hf-cached-ts-oc-data",
JSON.stringify(tornStatsData),
);
} catch (error) {
console.warn("TornStats Error:", error);
return;
}
},
onerror: function (response) {
console.error("Error fetching TornStats data:", response);
},
});
}
function fetchWeightData() {
const apiUrl = "https://tornprobability.com:3000/api/GetRoleWeights";
GM[httpRequest]({
method: "GET",
url: apiUrl,
responseType: "json",
onload: function (response) {
try {
response.response ??= JSON.parse(response.responseText); // In order for it to work with Torn PDA
const data = response.response;
weightData = data;
localStorage.setItem(
"hf-cached-oc-weight-data",
JSON.stringify(tornStatsData),
);
} catch (error) {
console.warn("Error fetching role weights:", error);
return;
}
},
onerror: function (response) {
console.error("Error fetching role weights:", response);
},
});
}
// HELPER function to create the SETTINGS modal
function createCPRmodal(username, userId, retries = 30) {
let tornStats = true;
let crimeData = tornStatsData[userId];
if (!crimeData) {
tornStats = false;
if (localData && localData[userId]) crimeData = localData[userId];
}
const mobile = !document.body.querySelector(
`[class*="searchFormWrapper__"]`,
);
const modal = document.createElement("div");
modal.classList.add("hf-modal");
document.body.appendChild(modal);
// Prevent body scrolling
const scrollY = window.scrollY;
document.body.style.position = "fixed";
document.body.style.top = `-${scrollY}px`;
document.body.style.width = "100%";
const cancelButton = document.createElement("button");
cancelButton.textContent = "✕";
cancelButton.classList.add("hf-cancel-btn");
cancelButton.addEventListener("click", function () {
document.body.style.position = "";
document.body.style.top = "";
document.body.style.width = "";
window.scrollTo(0, scrollY);
modal.remove();
});
modal.appendChild(cancelButton);
const titleContainer = document.createElement("div");
titleContainer.textContent = `${username} CPR Data`;
titleContainer.classList.add("hf-title");
modal.appendChild(titleContainer);
const subTitle = document.createElement("span");
if (tornStats) {
subTitle.textContent = "Gathered by TornStats";
} else if (crimeData) {
subTitle.textContent = "No TornStats data, using localStorage";
} else {
subTitle.textContent = "No data found";
}
subTitle.classList.add("hf-subtitle");
modal.appendChild(subTitle);
const scrollContainer = document.createElement("div");
scrollContainer.classList.add("hf-scroll-container");
modal.appendChild(scrollContainer);
const mainContainer = document.createElement("div");
mainContainer.classList.add("hf-main-container");
scrollContainer.appendChild(mainContainer);
if (!crimeData) return;
// Convert crimeData into an array so we can sort
const crimes = Object.keys(crimeData).map((crime) => {
if (!crimeLevelData[crime]) {
crimeLevelData[crime] = { level: 0, difficulty: "unknown" };
}
const level = crimeLevelData[crime].level;
const difficulty = crimeLevelData[crime].difficulty;
const difficultyTier =
Number(
Object.keys(difficultyTiers).find(
(key) => difficultyTiers[key] === difficulty,
),
) || 0; // unknown = 0
return { crime, level, difficulty, difficultyTier };
});
// Sort: difficultyTier high → low, then level high → low
crimes.sort((a, b) => {
if (a.difficultyTier !== b.difficultyTier) {
return b.difficultyTier - a.difficultyTier;
}
return b.level - a.level;
});
// Now loop in sorted order
for (const { crime, level, difficulty } of crimes) {
if (
settings[crime] &&
settings[crime].hidden &&
settings[crime].hidden === "true"
)
continue;
const crimeContainer = document.createElement("div");
crimeContainer.classList.add("hf-crime-container");
crimeContainer.classList.add(`hf-${difficulty}`);
mainContainer.appendChild(crimeContainer);
const crimeTitle = document.createElement("span");
crimeTitle.textContent = `${crime} (${level})`;
crimeTitle.classList.add("hf-crime-title");
crimeTitle.classList.add(`hf-${difficulty}`);
crimeContainer.appendChild(crimeTitle);
if (difficulty === "unknown")
crimeTitle.title = `Find this crime in any planned/finished crimes to complete the data`;
const scoreContainer = document.createElement("div");
scoreContainer.classList.add("hf-crime-score-container");
crimeContainer.appendChild(scoreContainer);
const roles = Object.entries(crimeData[crime]); // [[role1, 10], [role2, 5], [role3, 15]]
// Sort by score descending
roles.sort((a, b) => b[1] - a[1]);
// Iterate over sorted roles
for (const [role, score] of roles) {
const score = Number(crimeData[crime][role]);
const roleContainer = document.createElement("div");
roleContainer.classList.add("hf-role-container");
if (!settings[crime]) settings[crime] = {};
if (!settings[crime][role]) settings[crime][role] = 65;
if (score >= 75) {
roleContainer.classList.add("hf-good-cpr");
} else if (score >= 50) {
roleContainer.classList.add("hf-medium-cpr");
} else {
roleContainer.classList.add("hf-bad-cpr");
}
const roleSpan = document.createElement("span");
roleSpan.textContent = role;
roleSpan.classList.add("hf-crime-role-span");
roleContainer.appendChild(roleSpan);
const scoreSpan = document.createElement("span");
scoreSpan.textContent = score;
scoreSpan.classList.add("hf-crime-score-span");
roleContainer.appendChild(scoreSpan);
scoreContainer.appendChild(roleContainer);
}
}
return modal;
}
async function findOC(neededId, retries = 30) {
const crimes = document.body.querySelectorAll(`[class*="wrapper__"]`);
if (!crimes || crimes.length < 3) {
if (retries > 0) {
return new Promise((resolve) =>
setTimeout(() => resolve(findOC(neededId, retries - 1)), 100),
);
} else {
console.warn("[HF] Gave up looking for OCs after 30 retries.");
return null;
}
}
for (const crime of crimes) {
const crimeId = crime.getAttribute("data-oc-id");
if (neededId == crimeId) return crime;
}
}
async function findCrimeRoot(data, retries = 30) {
const crimeRoot = document.body.querySelector("#faction-crimes-root");
if (!crimeRoot) {
if (retries > 0) {
setTimeout(() => findCrimeRoot(data, retries - 1), 100);
} else {
console.warn("[HF] Gave up looking for crime root after 30 retries.");
}
return;
}
createObserver(crimeRoot, data);
}
async function findUninvolved(node, info, retries = 30) {
const uninvolved = node.querySelectorAll(
`[class*="list__"] [class*="item__"] a`,
);
if (!uninvolved || uninvolved.length < 1) {
if (retries > 0) {
setTimeout(() => findUninvolved(node, info, retries - 1), 100);
} else {
console.warn("[HF] Gave up looking for uninvolveds after 30 retries.");
return;
}
}
const lists = node.querySelectorAll(`[class*="list__"] [class*="item__"]`);
handleUninvoled(info, uninvolved, lists);
}
async function fetchMembers() {
const currentEpoch = Math.floor(Date.now() / 1000);
const fromTimestamp = currentEpoch - 7 * 24 * 60 * 60; // One week ago
const apiUrl = `https://api.torn.com/v2/faction/members?striptags=true&key=${apiKey}`;
try {
const response = await fetch(apiUrl);
const data = await response.json();
const statuses = {};
for (const member of data.members) {
statuses[member.id] = member.last_action.status;
}
return statuses;
} catch (error) {
console.error("Error fetching data: " + error);
return {}; // return empty object on error
}
}
function addSettingsTab(retries = 30) {
const btnContainer = document.body.querySelector(
`[class*="buttonsContainer__"]`,
);
let contentArea = document.getElementById("oc-content-area");
if (!contentArea)
contentArea = document.body.querySelector(
`#faction-crimes-root [class*="wrapper__"]`,
)?.parentNode;
if (!btnContainer || !contentArea) {
if (retries > 0) {
setTimeout(() => addSettingsTab(retries - 1), 100);
} else {
console.warn(
"[HF] Gave up looking for the button container after 30 retries.",
);
}
return;
}
const mobile = !document.body.querySelector(
`[class*="searchFormWrapper__]"`,
);
const otherButtons = btnContainer.querySelectorAll(
`#faction-crimes-root [class*="button__"]`,
);
const wrappers = contentArea.querySelectorAll(":scope > div");
const div = document.createElement("div");
div.classList.add("hf-cpr-config-container");
contentArea.appendChild(div);
showSettings(div);
const button = document.createElement("button");
button.textContent = "CPR Configuration";
if (mobile) button.textContent = "CPR";
button.classList.add("hf-cpr-config-btn");
btnContainer.appendChild(button);
btnContainer.addEventListener("click", function (e) {
const clicked = e.target.closest(`[class*="button__"]`);
if (!clicked) return;
if (clicked !== button) {
button.classList.forEach((cls) => {
if (cls.startsWith("active__")) button.classList.remove(cls);
});
div.classList.remove("hf-active");
button.classList.remove("hf-active");
}
});
button.addEventListener("click", function () {
for (const button of otherButtons) {
if (button.className.includes("active__"))
button.classList.forEach((cls) => {
if (cls.startsWith("active__")) button.classList.remove(cls);
});
}
for (const wrapper of wrappers) {
if (wrapper.classList.contains("hf-cpr-config-container")) continue;
wrapper.style.display = "none";
}
div.classList.add("hf-active");
button.classList.add("hf-active");
});
}
function showSettings(element) {
const mobile = !document.body.querySelector(
`[class*="searchFormWrapper__"]`,
);
const titleContainer = document.createElement("div");
titleContainer.classList.add("hf-title-container");
element.appendChild(titleContainer);
const title = document.createElement("div");
title.textContent = `CPR Requirements Configuration`;
if (mobile) title.textContent = `CPR Requirements Config`;
title.classList.add("hf-title");
titleContainer.appendChild(title);
const subTitle = document.createElement("span");
subTitle.textContent =
"Configure minimum CPR requirements for each crime and role";
subTitle.classList.add("hf-subtitle");
titleContainer.appendChild(subTitle);
const mainContainer = document.createElement("div");
mainContainer.classList.add("hf-main-container");
element.appendChild(mainContainer);
const deleteKey = document.createElement("span");
deleteKey.classList.add("hf-remove-key-span");
deleteKey.textContent = "Remove your API key";
deleteKey.addEventListener("click", function () {
removeAPIKey();
});
mainContainer.appendChild(deleteKey);
const toggleContainer = document.createElement("div");
toggleContainer.classList.add("hf-toggle-container");
mainContainer.appendChild(toggleContainer);
const highlightToggle = addToggle(
toggleContainer,
"Highlight unavailable and unfit members",
"highlight",
);
highlightToggle.addEventListener("change", function () {
if (highlightToggle.checked) settings.highlight = "true";
else settings.highlight = "false";
localStorage.setItem("hf-oc-cpr-settings", JSON.stringify(settings));
});
const weightToggle = addToggle(
toggleContainer,
"Set minimum requirements by weight instead of role (using Allenone [2033011]'s API endpoint)",
"weight",
);
weightToggle.addEventListener("change", function () {
if (weightToggle.checked) settings.weight = "true";
else settings.weight = "false";
localStorage.setItem("hf-oc-cpr-settings", JSON.stringify(settings));
});
const crimeArray = Object.entries(crimeLevelData);
const tierValues = Object.fromEntries(
Object.entries(difficultyTiers).map(([key, value]) => [
value,
Number(key),
]),
);
crimeArray.sort((a, b) => {
const crimeA = a[1];
const crimeB = b[1];
// Sort by difficulty tier (high → low)
const diffA = tierValues[crimeA.difficulty] || 0;
const diffB = tierValues[crimeB.difficulty] || 0;
if (diffB !== diffA) return diffB - diffA;
// Sort by level (high → low)
return Number(crimeB.level) - Number(crimeA.level);
});
// Convert back to object if needed
const sortedCrimeData = Object.fromEntries(crimeArray);
for (const crimeName in sortedCrimeData) {
if (!settings[crimeName]) settings[crimeName] = {};
const crime = sortedCrimeData[crimeName];
if (!crime.roles || crime.roles.length < 2) continue;
const crimeContainer = document.createElement("div");
crimeContainer.classList.add("hf-crime-container");
crimeContainer.classList.add(`hf-${crime.difficulty}`);
mainContainer.appendChild(crimeContainer);
const crimeTitleContainer = document.createElement("div");
crimeTitleContainer.classList.add("hf-crime-title-container");
crimeContainer.appendChild(crimeTitleContainer);
const crimeTitle = document.createElement("span");
crimeTitle.textContent = `${crimeName} (${crime.level})`;
crimeTitle.classList.add("hf-crime-title");
crimeTitle.classList.add(`hf-${crime.difficulty}`);
crimeTitleContainer.appendChild(crimeTitle);
const scoreContainer = document.createElement("div");
scoreContainer.classList.add("hf-crime-score-container");
crimeContainer.appendChild(scoreContainer);
const hideShowBtn = document.createElement("div");
let hidden = false;
if (!settings[crimeName].hidden) settings[crimeName].hidden = false;
if (settings[crimeName].hidden === "true") hidden = true;
hideShowBtn.classList.add("hf-oc-hide-show-btn");
function updateState() {
scoreContainer.classList.toggle("hf-hidden", hidden);
settings[crimeName].hidden = hidden ? "true" : "false";
localStorage.setItem("hf-oc-cpr-settings", JSON.stringify(settings));
hideShowBtn.innerHTML = hidden
? `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="feather feather-eye-off">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8
a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4
c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19
m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
<line x1="1" y1="1" x2="23" y2="23"></line></svg>`
: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="feather feather-eye">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11
8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>`;
}
updateState();
hideShowBtn.addEventListener("click", function () {
hidden = !hidden;
updateState();
});
crimeTitleContainer.appendChild(hideShowBtn);
if (settings.weight !== "true") {
for (const role of crime.roles.sort()) {
const roleContainer = document.createElement("div");
roleContainer.classList.add("hf-role-container");
scoreContainer.appendChild(roleContainer);
const roleSpan = document.createElement("span");
roleSpan.textContent = role;
roleSpan.classList.add("hf-crime-role-span");
roleContainer.appendChild(roleSpan);
const scoreInput = createNumberInput(crimeName, role, false);
scoreInput.classList.add("hf-crime-score-input");
roleContainer.appendChild(scoreInput);
}
} else {
if (!settings[crimeName]["weight"])
settings[crimeName]["weight"] = { 1: 65 };
function createWeightRow(weight, cpr, addNewRule) {
const roleContainer = document.createElement("div");
roleContainer.classList.add("hf-role-container");
if (!addNewRule) scoreContainer.appendChild(roleContainer);
else scoreContainer.insertBefore(roleContainer, addNewRule);
const roleSpan = document.createElement("span");
roleSpan.textContent = `At least ${weight}% weight`;
roleSpan.classList.add("hf-crime-role-span");
roleContainer.appendChild(roleSpan);
const cprBtnContainer = document.createElement("div");
cprBtnContainer.classList.add("hf-lc-weight-container");
roleContainer.appendChild(cprBtnContainer);
const scoreInput = createNumberInput(
crimeName,
"weight",
false,
weight,
);
scoreInput.classList.add("hf-crime-score-input");
cprBtnContainer.appendChild(scoreInput);
const button = document.createElement("button");
button.textContent = "×";
button.classList.add("hf-cpr-remove-btn");
cprBtnContainer.appendChild(button);
button.addEventListener("click", function () {
delete settings[crimeName]["weight"][weight];
// settings[crimeName]["weight"][value] = null;
localStorage.setItem(
"hf-oc-cpr-settings",
JSON.stringify(settings),
);
roleContainer.remove();
});
}
for (const [weight, cpr] of Object.entries(
settings[crimeName].weight,
).sort((a, b) => Number(a[0]) - Number(b[0]))) {
createWeightRow(weight, cpr);
}
const roleContainer = document.createElement("div");
roleContainer.classList.add("hf-role-container");
scoreContainer.appendChild(roleContainer);
const roleSpan = document.createElement("span");
roleSpan.textContent = "New weight rule: Enter minimum weight %";
roleSpan.classList.add("hf-crime-role-span");
roleContainer.appendChild(roleSpan);
const cprBtnContainer = document.createElement("div");
cprBtnContainer.classList.add("hf-lc-weight-container");
roleContainer.appendChild(cprBtnContainer);
const scoreInput = createNumberInput(crimeName, "changeweight", false);
scoreInput.classList.add("hf-crime-score-input");
cprBtnContainer.appendChild(scoreInput);
const button = document.createElement("button");
button.textContent = "+";
button.classList.add("hf-cpr-add-btn");
cprBtnContainer.appendChild(button);
button.addEventListener("click", function () {
settings[crimeName]["weight"][scoreInput.value] = 65;
localStorage.setItem("hf-oc-cpr-settings", JSON.stringify(settings));
createWeightRow(scoreInput.value, 65, roleContainer);
});
}
}
}
function createNumberInput(crimeName, role, element, weight) {
const className = crimeName.toLowerCase().replace(/\s+/g, "-");
const input = document.createElement("input");
input.classList.add("hf-number-input");
input.classList.add(className);
input.type = "number";
input.min = 1;
input.max = 100;
if (role !== "changeweight" && role !== "weight")
input.value = settings[crimeName]?.[role] ?? 65;
else if (role === "changeweight") input.value = 1;
else if (role === "weight")
input.value = settings[crimeName]?.["weight"]?.[weight] ?? 65;
if (element) element.appendChild(input);
if (role !== "changeweight") {
input.addEventListener("input", function () {
settings[crimeName] ??= {};
settings[crimeName][role] ??= {};
if (role !== "weight") {
settings[crimeName][role] = input.value;
} else {
settings[crimeName].weight ??= {};
settings[crimeName].weight[weight] = input.value;
}
localStorage.setItem("hf-oc-cpr-settings", JSON.stringify(settings));
});
}
return input;
}
function addToggle(element, content, settingsName) {
const container = document.createElement("div");
container.classList.add("hf-toggle-subcontainer");
const label = document.createElement("label");
label.classList.add("hf-switch");
const text = document.createElement("span");
text.classList.add("hf-input-text");
text.textContent = content;
const input = document.createElement("input");
input.type = "checkbox";
input.classList.add("hf-checkbox");
const slider = document.createElement("span");
slider.classList.add("hf-slider", "round");
if (settings[settingsName] === "true") input.checked = true;
label.appendChild(input);
label.appendChild(slider);
container.appendChild(label);
container.appendChild(text);
element.appendChild(container);
return input;
}
// HELPER function to create a mutation observer and check nerve
function createObserver(element, info) {
let target;
target = element;
if (!target) {
console.error(`[HF] Mutation Observer target not found.`);
return;
}
const observer = new MutationObserver(function (mutationsList, observer) {
for (const mutation of mutationsList) {
if (mutation.type === "childList") {
mutation.addedNodes.forEach((node) => {
if (
node.classList &&
node.className.includes("notInvolvedMembers__")
) {
findUninvolved(node, info);
}
});
}
}
});
const config = {
attributes: true,
childList: true,
subtree: true,
characterData: true,
};
observer.observe(target, config);
}
function addStyle(css) {
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
}
function runScript() {
if (document.hfOCTrackCpr) return;
if (
window.location.href.includes("factions") &&
window.location.href.includes("tab=crimes")
) {
document.hfOCTrackCpr = true;
hookFetch(window);
if (typeof unsafeWindow !== "undefined") hookFetch(unsafeWindow);
fetchTornStatsData();
if (settings.weight) setTimeout(fetchWeightData, 1000);
addSettingsTab();
} else {
document.hfOCTrackCpr = false;
}
}
runScript();
let lastUrl = location.href;
new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
runScript();
}
}).observe(document, { subtree: true, childList: true });
// Styles
addStyle(`
.hf-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 20px 20px 0px 20px;
background-color: var(--sidebar-area-bg-attention);
border: 2px solid var(--default-tabs-color);
border-radius: 15px;
max-width: fit-content;
width: 60vw;
z-index: 9999;
max-height: 75vh;
display: flex;
flex-direction: column;
line-height: normal;
}
.hf-cancel-btn {
position: absolute;
right: 10px;
top: -10px;
cursor: pointer;
background-color: #CCC;
color: black;
border-radius: 99px;
z-index: 9;
font-size: medium;
}
.hf-title-container {
display: flex;
flex-direction: column;
align-items: center;
margin: 0 auto;
}
.hf-title {
font-size: x-large;
font-weight: bolder;
text-align: center;
text-wrap: balance;
}
.hf-subtitle {
text-align: center;
padding-bottom: 8px;
padding-top: 4px;
}
.hf-scroll-container {
max-height: 100%;
flex: 1;
overflow-y: auto;
margin-top: 8px;
padding-bottom: 20px;
}
.hf-main-container {
margin: 0 auto;
display: flex;
flex-direction: column;
}
.hf-crime-title-container {
display: flex;
justify-content: space-between;
align-items: center;
}
.hf-crime-container {
background: #516574;
border-radius: 5px;
margin: 5px;
border-left: 4px solid grey;
padding: 8px;
}
.hf-crime-title {
font-size: 15px;
font-weight: bold;
}
.hf-crime-score-container {
padding-top: 8px;
margin-bottom: -4px;
}
.hf-crime-score-container.hf-hidden {
display: none;
}
.hf-role-container {
padding: 4px;
display: flex;
justify-content: space-between;
align-items: center;
border: 2px solid #989898;
border-radius: 5px;
margin-bottom: 6px;
background: #647b8c
}
.hf-crime-score-span {
font-weight: bold;
}
.hf-crime-role-span {
color: white;
}
.hf-crime-score-input {
padding: 5px;
border-radius: 5px;
border: 1px solid #ccc;
width: 35px;
height: 5px;
margin-left: 5px;
background: #516574;
color: #fff;
}
.hf-cpr-icon {
cursor: pointer;
margin-right: 10px;
}
.hf-oc-hide-show-btn {
cursor: pointer;
display: flex;
}
.hf-oc-hide-show-btn svg {
width: 20px;
height: 20px;
}
.hf-bad-cpr {
background: #ff794c40;
}
.hf-bad-cpr .hf-crime-score-span {
color: #ff794c !important;
}
.hf-medium-cpr {
background: #fcc41940;
}
.hf-medium-cpr .hf-crime-score-span {
color: #fcc419 !important;
}
.hf-good-cpr {
background: #94d82d40;
}
.hf-good-cpr .hf-crime-score-span {
color: #94d82d !important;
}
.hf-crime-title.hf-introductory {
color: #8ce99a;
}
.hf-crime-container.hf-introductory {
border-color: #8ce99a !important;
}
.hf-crime-title.hf-simple {
color: #ffe066;
}
.hf-crime-container.hf-simple {
border-color: #ffe066 !important;
}
.hf-crime-title.hf-intermediate {
color: #ffa94d;
}
.hf-crime-container.hf-intermediate {
border-color: #ffa94d !important;
}
.hf-crime-title.hf-advanced {
color: #ff8787;
}
.hf-crime-container.hf-advanced {
border-color: #ff8787 !important;
}
.hf-crime-title.hf-elaborate {
color: #b197fc;
}
.hf-crime-container.hf-elaborate {
border-color: #b197fc !important;
}
.hf-crime-title.hf-unknown {
color: #9e9e9e;
}
.hf-crime-container.hf-unknown {
border-color: #9e9e9e !important;
}
.hf-activity-icon {
background-image: url(https://www.torn.com/images/v2/svg_icons/sprites/user_status_icons_sprite.svg);
height: 17px;
width: 17px;
margin-right: 4px;
cursor: pointer;
}
.hf-cpr-config-btn {
font-size: 12px;
background: var(--tabs-bg-gradient);
color: var(--tabs-color);
cursor: pointer;
text-align: center;
border: none;
flex: 1 0 0;
height: 33px;
padding-bottom: 2px;
font-weight: 700;
position: relative;
}
.hf-cpr-config-container {
width: max-content;
max-width: 90vw;
justify-self: center;
display: none;
margin: 0 auto;
}
.hf-cpr-config-btn.hf-active {
background: var(--tabs-active-bg-gradient);
}
.hf-cpr-config-container.hf-active {
display: block !important;
}
.hf-toggle-container {
align-self: center;
padding: 8px;
display: flex;
flex-direction: column;
}
.hf-toggle-subcontainer {
padding-bottom: 4px;
}
.hf-input-text {
padding-left: 5px;
}
.hf-switch {
position: relative;
display: inline-block;
width: 20px;
height: 10px;
top: 1px;
}
.hf-switch input {
opacity: 0;
width: 0;
height: 0;
}
.hf-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #ccc;
transition: .4s;
}
.hf-slider:before {
position: absolute;
content: "";
height: 10px;
width: 10px;
background-color: white;
transition: .4s;
}
input:checked + .hf-slider {
background-color: #2196F3;
}
input:focus + .hf-slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .hf-slider:before {
transform: translateX(10px);
}
.hf-slider.round {
border-radius: 34px;
}
.hf-slider.round:before {
border-radius: 50%;
}
.hf-remove-key-span {
color: var(--default-blue-color);
padding: 4px;
align-self: center;
cursor: pointer;
}
.hf-cpr-data-btn {
color: var(--oc-slot-menu-text);
cursor: pointer;
text-align: center;
width: 100%;
padding: 8px 0;
font-size: 12px;
text-decoration: none;
display: block;
}
.hf-cpr-data-btn:not(:last-child) {
border-bottom: 1px solid var(--oc-border-slot-player);
}
.hf-cpr-remove-btn {
background: #ff000061;
color: #CCC;
border-radius: 5px;
cursor: pointer;
}
.hf-cpr-add-btn {
background: #003a047d;
color: #CCC;
border-radius: 5px;
cursor: pointer;
}
.hf-lc-weight-container {
display: flex;
gap: 4px;
align-items: center;
}
.hf-lc-weight-container input {
width: 45px !important;
}
`);
const colorScheme = {
"bad-cpr": "#ff794c",
"medium-cpr": "#fcc419",
"good-cpr": "#94d82d",
introductory: "#8ce99a",
simple: "#ffe066",
intermediate: "#ffa94d",
advanced: "#ff8787",
elaborate: "#b197fc",
};
})();