OC Track CPR

Show CPR data, add status icon to unavailable, highlight member inefficiency

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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