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