// ==UserScript==
// @name Loadout Reveal
// @namespace http://tampermonkey.net/
// @version 1.0
// @description NST: Loadout reveal for those in gunshops
// @author Hwa [2466470]
// @match https://www.torn.com/loader.php?sid=attack*
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Wait until a selector exists anywhere
function waitForSelector(selector, callback, root = document.body) {
const el = root.querySelector(selector);
if (el) return callback(el);
const observer = new MutationObserver((mutations, obs) => {
const el = root.querySelector(selector);
if (el) {
obs.disconnect();
callback(el);
}
});
observer.observe(root, { childList: true, subtree: true });
}
// extract armor
function extractArmor(playerEl) {
const armorAreas = playerEl.querySelectorAll('area');
const armor = {};
['Helmet','Chest','Gloves','Pants','Boots'].forEach(part => {
const area = Array.from(armorAreas).find(a => a.alt === part);
armor[part.toLowerCase()] = area ? (area.title || "N/A") : "N/A";
});
return armor;
}
// Extract armor prefix
function getArmorPrefix(armor) {
const parts = ['helmet', 'chest', 'gloves', 'pants', 'boots'];
const names = parts.map(part => armor[part]);
if (names.some(name => !name)) return null;
const prefixes = names.map(name => name.split(' ')[0]);
return prefixes.every(p => p === prefixes[0]) ? prefixes[0] : null;
}
// extract weapons
function extractWeapons(playerEl) {
const weaponListDiv = playerEl.querySelector('[class^="weaponList"]');
if (!weaponListDiv) return {};
const weapons = {};
['weapon_main','weapon_second','weapon_melee'].forEach(id => {
const div = weaponListDiv.querySelector(`#${id}`);
if (div) {
const img = div.querySelector('figure img');
const weaponName = img?.alt || 'N/A';
const damage = div.querySelector('[id^="damage-value_"]')?.textContent || 'N/A';
const accuracy = div.querySelector('span[id^="accuracy_"]')?.textContent || 'N/A';
const bonus_attachments = Array.from(div.querySelectorAll('[data-bonus-attachment-title]'))
.map(i => ({
title: i.getAttribute('data-bonus-attachment-title') || '',
description: i.getAttribute('data-bonus-attachment-description') || ''
})).filter(b => b.title);
weapons[id.replace('weapon_','').replace('main','Primary').replace('second','Secondary').replace('melee','Melee')] = {
weapon: weaponName,
damage,
accuracy,
bonus_attachments
};
} else {
weapons[id.replace('weapon_','').replace('main','Primary').replace('second','Secondary').replace('melee','Melee')] = 'N/A';
}
});
// Temp weapon
weapons.Temp = weaponListDiv.querySelector('#weapon_temp figure img')?.alt || 'N/A';
return weapons;
}
// Format weapon info
function formatWeaponData(weaponData) {
if (!weaponData || typeof weaponData !== 'object' || !weaponData.weapon) return "N/A";
let text = `${weaponData.weapon}, damage: ${weaponData.damage}, accuracy: ${weaponData.accuracy}`;
if (Array.isArray(weaponData.bonus_attachments) && weaponData.bonus_attachments.length) {
text += '<br>Bonuses:';
for (const bonus of weaponData.bonus_attachments) {
text += `<br>- ${bonus.title} (${bonus.description.replace('+','').trim()})`;
}
}
return text;
}
// Create table with copy button
function createDataTable(dataObj) {
const container = document.createElement('div');
container.style.display = 'flex';
container.style.alignItems = 'flex-start';
container.style.marginBottom = '10px';
container.style.gap = '10px';
const table = document.createElement('table');
table.style.borderCollapse = 'collapse';
table.style.background = '#111';
table.style.color = '#fff';
table.style.fontFamily = 'monospace';
table.style.zIndex = '9999';
table.style.position = 'relative';
for (const [key, value] of Object.entries(dataObj)) {
const row = document.createElement('tr');
const keyCell = document.createElement('td');
keyCell.textContent = key;
keyCell.style.border = '1px solid #888';
keyCell.style.padding = '4px';
keyCell.style.background = '#222';
keyCell.style.color = '#fff';
const valueCell = document.createElement('td');
if (value && typeof value === 'object' && value.weapon) {
valueCell.innerHTML = formatWeaponData(value);
} else {
valueCell.textContent = value;
}
valueCell.style.border = '1px solid #888';
valueCell.style.padding = '4px';
valueCell.style.background = '#333';
valueCell.style.color = '#fff';
row.appendChild(keyCell);
row.appendChild(valueCell);
table.appendChild(row);
}
const button = document.createElement('button');
button.textContent = 'Copy';
button.style.marginLeft = '5px';
button.style.padding = '2px 5px';
button.style.backgroundColor = '#444';
button.style.color = '#fff';
button.style.border = '1px solid #888';
button.style.borderRadius = '4px';
button.style.cursor = 'pointer';
button.style.fontFamily = 'monospace';
button.style.fontSize = '14px';
button.addEventListener('click', () => {
const rows = Array.from(table.querySelectorAll('tr')).map(row => {
const cells = row.querySelectorAll('td');
return `${cells[0].textContent}: ${cells[1].innerText}`;
});
navigator.clipboard.writeText(rows.join('\n')).then(() => {
button.textContent = 'Copied!';
setTimeout(() => button.textContent = 'Copy', 2000);
});
});
container.appendChild(table);
container.appendChild(button);
return container;
}
// Insert table
function insertTable(data) {
const container = document.querySelector('[class^="coreWrap"]');
if (!container) return;
const afterEl = container.querySelector('[class^="logStatsWrap__"]');
const tableEl = createDataTable(data);
afterEl ? afterEl.after(tableEl) : container.appendChild(tableEl);
return container;
}
// Extract data from a player element
function extractData(playerEl, nameEl) {
// Armor
const armor = extractArmor(playerEl);
const armorPrefix = getArmorPrefix(armor);
// Weapons
const weapons = extractWeapons(playerEl);
const data = {
...(armorPrefix ? { 'Full Set': armorPrefix } : armor),
...weapons,
};
console.log("Extracted Data:", data);
return data;
}
// ------------- MOBILE ONLY ---------------
function getPageType() {
// Look for any weapon slot element
const weaponEl = document.querySelector('[id^="weapon_main"]');
if (!weaponEl) return 'unknown';
// If the weapon element is for the defender, we’re on the second tab
const isDefender = Array.from(weaponEl.classList).some(c => c.includes('defender_'));
if (isDefender) {
return 'second'; // Defender's weapon view (switched tab)
}
return 'first'; // Default (defender armor + your weapons)
}
function observeMobileWeapons(callback) {
const containerSelector = 'div[class^="weaponList_"]';
const tryAttach = () => {
const container = document.querySelector(containerSelector);
if (!container) {
console.warn("Weapon container not found yet, retrying...");
setTimeout(tryAttach, 500);
return;
}
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
// Only look for added nodes
if (mutation.type === 'childList' && mutation.addedNodes.length) {
mutation.addedNodes.forEach(node => {
if (!(node instanceof HTMLElement)) return;
const weaponMain = node.id?.startsWith('weapon_main') ? node : node.querySelector('[id^="weapon_main"]');
if (!weaponMain) return;
const isDefender = Array.from(weaponMain.classList).some(c => c.includes('defender_'));
callback(isDefender ? 'second' : 'first', weaponMain);
});
}
}
});
observer.observe(container, {
childList: true,
subtree: true
});
// Initial detection in case it’s already loaded
const weaponMain = container.querySelector('[id^="weapon_main"]');
if (weaponMain) {
const isDefender = Array.from(weaponMain.classList).some(c => c.includes('defender_'));
callback(isDefender ? 'second' : 'first', weaponMain);
}
};
tryAttach();
}
function updateDataTable(container, dataObj) {
if (!container) return;
const table = container.querySelector('table');
if (!table) return;
for (const [key, value] of Object.entries(dataObj)) {
let row = table.querySelector(`tr[data-key="${key}"]`);
if (!row) {
// Create new row if it doesn't exist
row = document.createElement('tr');
row.dataset.key = key;
const keyCell = document.createElement('td');
keyCell.textContent = key;
keyCell.style.border = '1px solid #888';
keyCell.style.padding = '4px';
keyCell.style.background = '#222';
keyCell.style.color = '#fff';
const valueCell = document.createElement('td');
valueCell.style.border = '1px solid #888';
valueCell.style.padding = '4px';
valueCell.style.background = '#333';
valueCell.style.color = '#fff';
row.appendChild(keyCell);
row.appendChild(valueCell);
table.appendChild(row);
}
// Update row content
const valueCell = row.querySelector('td:nth-child(2)');
if (value && typeof value === 'object' && value.weapon) {
valueCell.innerHTML = formatWeaponData(value);
} else {
valueCell.textContent = value;
}
}
}
// Wait for full fight page
waitForSelector('[class^="playerArea_"]', playerAreas => {
const players = document.querySelectorAll('[class^="playerArea_"]');
const isMobile = players.length === 1; // only one player area = mobile view
console.log("Is mobile?", isMobile);
const nameEls = document.querySelectorAll('span[class*="userName___"][class*="user-name"]');
const dataTableContainer = insertTable({ Name: nameEls[1]?.textContent.trim() || 'N/A' });
if (isMobile) {
observeMobileWeapons((pageType, el) => {
const playerEl = document.querySelector('[class^="playerArea_"]');
if (pageType === 'first') {
const armor = extractArmor(playerEl);
const armorPrefix = getArmorPrefix(armor);
updateDataTable(dataTableContainer, armorPrefix ? { 'Full Set': armorPrefix } : armor);
} else {
const weapons = extractWeapons(playerEl);
updateDataTable(dataTableContainer, weapons);
}
});
} else {
const secondPlayer = players[1];
const data = extractData(secondPlayer);
updateDataTable(dataTableContainer, data);
}
});
})();