TORN: True Weapon XP Viewer

Shows total weapon xp hits for all currently obtainable civilian weapons

// ==UserScript==
// @name         TORN: True Weapon XP Viewer
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Shows total weapon xp hits for all currently obtainable civilian weapons
// @author       tonyrussin [2135411]
// @match        https://www.torn.com/item.php
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function () {
    "use strict";

    const apiKey = "";

    // Big thanks goes to DeKleineKobini [2114440] for helping get over the last roadbumps and making it look pretty

    // Same weapons from tornstats except these are missing (and also non-damaging temps):
    // If you want to include them, add their ids to their appropriate list
    //
    // Primary: None
    // Secondary: Tranquilizer Gun [844], Prototype [874]
    // Melee: Bolt Gun [845], Scalpel [846], Bug Swatter [871], Millwall Brick [1056], Ban Hammer [1296]
    // Temporary: Book [581] (Due to unstable supply), Semtex [1054]

    const primaryList = [
        22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 63, 76, 98, 100, 108, 174, 219, 223, 225, 228, 231, 232, 241, 249, 252, 382, 398, 399, 484, 487, 488, 545, 546,
        547, 548, 549, 612, 830, 837, 1155, 1156, 1157,
    ];

    const secondaryList = [
        12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 99, 109, 175, 177, 189, 218, 230, 233, 240, 243, 244, 248, 253, 254, 255, 388, 393, 483, 485, 486, 489, 490,
        613, 831, 838, 1152, 1153, 1154,
    ];

    const meleeList = [
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 110, 111, 146, 147, 170, 173, 217, 224, 227, 234, 235, 236, 237, 238, 245, 247, 250, 251, 289, 290, 291, 292, 346,
        359, 360, 387, 391, 395, 397, 400, 401, 402, 438, 439, 440, 539, 599, 600, 604, 605, 614, 615, 632, 790, 792, 805, 832, 839, 850, 1053, 1055, 1158,
        1159, 1173, 1231, 1255, 1257,
    ];

    const temporaryList = [220, 221, 229, 239, 242, 246, 257, 394, 611, 616, 742, 840, 1205];

    async function pullWeaponXpData() {
        try {
            const weaponXPJson = await fetch(`https://api.torn.com/user/?selections=weaponexp&key=${apiKey}&comment=WeaponXPViewer`);
            const data = await weaponXPJson.json();
            const dataWeaponXp = data.weaponexp;

            processWeaponExperience(dataWeaponXp);
            saveToCache(dataWeaponXp);
        } catch (error) {
            console.error("Error during API call:", error);
        }
    }

    function processWeaponExperience(data) {
        const categories = [
            ["Primary", primaryList],
            ["Secondary", secondaryList],
            ["Melee", meleeList],
            ["Temporary", temporaryList],
        ];

        const results = [];
        for (const [title, list] of categories) {
            const totals = calculateExperience(data, list);

            results.push([title, totals]);
        }

        results.push(["Total", calculateTotal(results.map(([, result]) => result))]);

        updateData(results);
    }

    function calculateExperience(weaponexpdata, weaponlist) {
        let xpTotal = 0;
        let xpMaxTotal = weaponlist.length * 2000;
        let weaponCompleteTotal = 0;
        for (let i = 0; i < weaponlist.length; i++) {
            const itemID = weaponlist[i];
            const weapon = weaponexpdata.find((w) => w.itemID === itemID);
            if (weapon) {
                const weaponXpDone = calculateRemainingHits(weapon.exp);
                if (weaponXpDone === 2000) {
                    weaponCompleteTotal += 1;
                }
                xpTotal += weaponXpDone;
            } else {
                xpTotal += calculateRemainingHits(0);
            }
        }
        return {
            gainedXp: xpTotal,
            totalPossibleXp: xpMaxTotal,
            maximumTotals: xpMaxTotal / 2000,
            completeTotals: weaponCompleteTotal,
        };
    }

    function calculateTotal(results) {
        return {
            gainedXp: results.map((r) => r.gainedXp).reduce((prev, val) => prev + val, 0),
            totalPossibleXp: results.map((r) => r.totalPossibleXp).reduce((prev, val) => prev + val, 0),
            maximumTotals: results.map((r) => r.maximumTotals).reduce((prev, val) => prev + val, 0),
            completeTotals: results.map((r) => r.completeTotals).reduce((prev, val) => prev + val, 0),
        };
    }

    function calculateRemainingHits(percentage) {
        if (percentage === 100) {
            return 2000;
        } else if (percentage >= 76 && percentage <= 99) {
            return 2000 - (100 - percentage) * 40;
        } else if (percentage >= 51 && percentage <= 75) {
            return 2000 - ((75 - percentage) * 20 + 1000);
        } else if (percentage >= 26 && percentage <= 50) {
            return 2000 - ((50 - percentage) * 12 + 1500);
        } else if (percentage >= 1 && percentage <= 25) {
            return 2000 - ((25 - percentage) * 8 + 1800);
        } else {
            return 0;
        }
    }

    GM_addStyle(`
        #xp-viewer-header {
            background: repeating-linear-gradient(90deg, #2e2e2e, #2e2e2e 2px, #282828 0, #282828 4px);
            height: 22px;
            line-height: 22px;
            display: flex;
            justify-content: space-between;
            border-top-left-radius: 8px; /* Adjust as needed */
            border-top-right-radius: 8px; /* Adjust as needed */
        }

        #xp-viewer-header h2 {
            font-size: 12px;
            font-weight: 700;
            color: var(--sidebar-titles-font-color, #fff);
            margin: 0;
            padding-left: 10px;
            text-shadow: 0 1px 0 #333;
        }

        #xp-viewer-content {
            background-color: var(--default-bg-panel-color);
            padding: 4px 8px;
            border-bottom-left-radius: 8px; /* Adjust as needed */
            border-bottom-right-radius: 8px; /* Adjust as needed */
        }

        #xp-viewer-refresh {
            color: var(--sidebar-titles-font-color, #fff);
            cursor: pointer;
            font-size: 10px;
        }

        .xp-viewer-entry {
            margin-block: 4px;
        }

        #xp-viewer-menu {
            margin-bottom: 2px;
        }
    `);

    function displayMenu() {
        const title = document.createElement("h2");
        title.textContent = "Weapon XP";

        const refreshButton = document.createElement("button");
        refreshButton.id = "xp-viewer-refresh";
        refreshButton.title = "Refresh";
        refreshButton.innerHTML = '<i class="fas fa-sync-alt"></i>';

        refreshButton.addEventListener("click", pullWeaponXpData);
        refreshButton.style.background = "none";
        refreshButton.style.border = "none";
        refreshButton.style.cursor = "pointer";
        refreshButton.style.color = "var(--sidebar-titles-font-color, #fff)";
        refreshButton.style.fontSize = "11px";
        refreshButton.style.marginRight = "8px";

        const header = document.createElement("div");
        header.id = "xp-viewer-header";
        header.appendChild(title);
        header.appendChild(refreshButton);
        header.style.display = "flex";
        header.style.alignItems = "center";
        header.style.justifyContent = "space-between";

        const content = document.createElement("div");
        content.id = "xp-viewer-content";

        const menu = document.createElement("div");
        menu.id = "xp-viewer-menu";
        menu.style.display = "";
        menu.appendChild(header);
        menu.appendChild(content);

        const areas = document.evaluate(
            `//*[@id="sidebar"]//h2[text()="Areas"]/ancestor::div[contains(@class, 'sidebar-block___')]`,
            document,
            null,
            XPathResult.FIRST_ORDERED_NODE_TYPE,
            null,
        )?.singleNodeValue;

        if (typeof areas === "undefined") throw new Error("Couldn't find the areas block.");

        areas.insertAdjacentElement("beforebegin", menu);
    }

    function updateData(results) {
        const menu = document.getElementById("xp-viewer-menu");
        menu.style.display = "";

        const content = document.getElementById("xp-viewer-content");
        [...content.children].forEach((node) => node.remove());

        results.forEach(([title, result]) => {
            const category = document.createElement("span");
            category.textContent = `${title} (${result.completeTotals}/${result.maximumTotals})`;

            const hits = document.createElement("span");
            hits.textContent = `${result.gainedXp}/${result.totalPossibleXp} = ${((result.gainedXp / result.totalPossibleXp) * 100).toFixed(3)}%`;

            const entry = document.createElement("div");
            entry.classList.add("xp-viewer-entry");
            entry.appendChild(category);
            entry.appendChild(document.createElement("br"));
            entry.appendChild(hits);

            content.appendChild(entry);
        });
    }

    function saveToCache(data) {
        localStorage.setItem("weapon-xp-viewer", JSON.stringify(data));
    }

    function loadFromCache() {
        const stored = localStorage.getItem("weapon-xp-viewer");
        if (!stored) return;

        return JSON.parse(stored);
    }

    function observeSidebarLoad() {
    const sidebarXPath = `//*[@id="sidebar"]//h2[text()="Areas"]/ancestor::div[contains(@class, 'sidebar-block___')]`;
    const sidebar = document.evaluate(sidebarXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)?.singleNodeValue;
    if (sidebar) {
        displayMenu();
        const data = loadFromCache();
        if (data) {
            processWeaponExperience(data);
        }
    }
    else {
        const observer = new MutationObserver((mutations, obs) => {
            const sidebarLoaded = document.evaluate(sidebarXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null)?.singleNodeValue;
            if (sidebarLoaded) {
                displayMenu();
                obs.disconnect();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true});
        }
    }
    observeSidebarLoad();

})();