// ==UserScript==
// @name Loadout Reveal
// @namespace http://tampermonkey.net/
// @version 0.3
// @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';
let alreadyExtracted = false;
function debounce(func, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), delay);
};
}
function getArmorPrefix(armor) {
const parts = ['helmet', 'chest', 'gloves', 'pants', 'boots'];
const names = parts.map(part => armor[part]);
// Return null if any part is missing
if (names.some(name => !name)) return null;
const prefixes = names.map(name => name.split(' ')[0]);
const allSame = prefixes.every(prefix => prefix === prefixes[0]);
return allSame ? prefixes[0] : null;
}
function formatWeaponData(weaponData) {
if (!weaponData || typeof weaponData !== 'object') return "N/A";
const { weapon, damage, accuracy, bonus_attachments } = weaponData;
if (!weapon) return "N/A";
let mainLine = `${weapon}, damage: ${damage}, accuracy: ${accuracy}`;
let bonusesText = '';
if (Array.isArray(bonus_attachments) && bonus_attachments.length > 0) {
bonusesText = '<br>Bonuses:';
for (const bonus of bonus_attachments) {
const title = bonus.title.toLowerCase();
const description = bonus.description.replace('+', '').trim();
bonusesText += `<br>- ${title} (${description})`;
}
}
return mainLine + bonusesText;
}
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'; // space between table and button
const table = document.createElement('table');
table.id = "player_loadout_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.color = '#fff';
keyCell.style.background = '#222';
const valueCell = document.createElement('td');
if (value && typeof value === 'object' && value.weapon) {
valueCell.innerHTML = formatWeaponData(value);
} else if (typeof value === 'object') {
// For other objects, just stringify nicely with indentation
valueCell.textContent = JSON.stringify(value, null, 2);
} else {
valueCell.textContent = value;
}
valueCell.style.border = '1px solid #888';
valueCell.style.padding = '4px';
valueCell.style.color = '#fff';
valueCell.style.background = '#333';
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';
// Copy logic
button.addEventListener('click', () => {
const rows = Array.from(table.querySelectorAll('tr')).map(row => {
const cells = row.querySelectorAll('td');
const label = cells[0].textContent.trim();
const content = cells[1].innerText.trim(); // innerText preserves line breaks
return `${label}: ${content}`;
});
const text = rows.join('\n');
navigator.clipboard.writeText(text).then(() => {
button.textContent = 'Copied!';
setTimeout(() => (button.textContent = 'Copy'), 2000);
});
});
// Append table and button to container
container.appendChild(table);
container.appendChild(button);
return container;
}
function insertTable(data) {
const table = createDataTable(data);
const container = document.querySelector('[class^="coreWrap"]')
const childToInsertAfter = container.querySelector('[class^="logStatsWrap__"]'); // choose the child element you want
if (childToInsertAfter && container) {
// Insert after the chosen child:
childToInsertAfter.after(table);
} else if (container) {
// fallback: append at the end
container.appendChild(table);
}
}
function extractData() {
if (alreadyExtracted) return;
const players = document.querySelectorAll('[class^="player___"]');
let secondPlayer = null;
// Check if there are at least two elements
if (players.length >= 2) {
secondPlayer = players[1]; // 0-based index, so 1 is the second element
console.log("Second player found");
} else {
console.log("Less than two elements found");
}
if (secondPlayer) {
alreadyExtracted = true;
const nameEl = secondPlayer.querySelector('[id^="playername_"]');
let xid = "N/A";
if (xid === "N/A" && window.location.href.includes('user2ID=')) {
const match = window.location.href.match(/user2ID=(\d+)/);
if (match) xid = match[1];
}
const armorAreas = secondPlayer.querySelectorAll('area');
const armor = {};
['Helmet', 'Chest', 'Gloves', 'Pants', 'Boots'].forEach(part => {
const area = Array.from(armorAreas).find(el => el.alt === part);
if (area) {
armor[part.toLowerCase()] = area.title || "N/A";
}
});
const weaponListDiv = secondPlayer.querySelector('div[class^="weaponList"]');
const weaponMainDiv = weaponListDiv.querySelector('#weapon_main');
let primary = {};
if (weaponMainDiv) {
// Weapon name from img alt (replace spaces with hyphen or keep as is)
const weaponImg = weaponMainDiv.querySelector('figure[class^="weaponImage"] img');
const weaponName = weaponImg ? weaponImg.alt.trim() : "N/A";
// Damage value
const damageSpan = weaponMainDiv.querySelector('[id^="damage-value_"]');
const damage = damageSpan ? damageSpan.textContent.trim() : "N/A";
// Accuracy value
const accuracySpan = weaponMainDiv.querySelector('span[id^="accuracy_"]');
const accuracy = accuracySpan ? accuracySpan.textContent.trim() : "N/A";
// Bonus attachments (titles and descriptions)
const bonusAttachments = [...weaponMainDiv.querySelectorAll('[class^="props"] i[data-bonus-attachment-title]')]
.map(i => {
return {
title: i.getAttribute('data-bonus-attachment-title') || "",
description: i.getAttribute('data-bonus-attachment-description') || ""
};
})
// filter out empty titles if needed
.filter(b => b.title);
primary = {
weapon: weaponName,
damage: damage,
accuracy: accuracy,
bonus_attachments: bonusAttachments
};
}
const weaponSecondaryDiv = weaponListDiv.querySelector('#weapon_second');
let secondary = {};
if (weaponSecondaryDiv) {
// Weapon name from img alt (replace spaces with hyphen or keep as is)
const weaponImg = weaponSecondaryDiv.querySelector('figure[class^="weaponImage"] img');
const weaponName = weaponImg ? weaponImg.alt.trim() : "N/A";
// Damage value
const damageSpan = weaponSecondaryDiv.querySelector('[id^="damage-value_"]');
const damage = damageSpan ? damageSpan.textContent.trim() : "N/A";
// Accuracy value
const accuracySpan = weaponSecondaryDiv.querySelector('span[id^="accuracy_"]');
const accuracy = accuracySpan ? accuracySpan.textContent.trim() : "N/A";
// Bonus attachments (titles and descriptions)
const bonusAttachments = [...weaponSecondaryDiv.querySelectorAll('[class^="props"] i[data-bonus-attachment-title]')]
.map(i => {
return {
title: i.getAttribute('data-bonus-attachment-title') || "",
description: i.getAttribute('data-bonus-attachment-description') || ""
};
})
// filter out empty titles if needed
.filter(b => b.title);
secondary = {
weapon: weaponName,
damage: damage,
accuracy: accuracy,
bonus_attachments: bonusAttachments
};
}
const weaponMeleeDiv = weaponListDiv.querySelector('#weapon_melee');
let melee = {};
if (weaponMeleeDiv) {
// Weapon name from img alt (replace spaces with hyphen or keep as is)
const weaponImg = weaponMeleeDiv.querySelector('figure[class^="weaponImage"] img');
const weaponName = weaponImg ? weaponImg.alt.trim() : "N/A";
// Damage value
const damageSpan = weaponMeleeDiv.querySelector('[id^="damage-value_"]');
const damage = damageSpan ? damageSpan.textContent.trim() : "N/A";
// Accuracy value
const accuracySpan = weaponMeleeDiv.querySelector('span[id^="accuracy_"]');
const accuracy = accuracySpan ? accuracySpan.textContent.trim() : "N/A";
// Bonus attachments (titles and descriptions)
const bonusAttachments = [...weaponMeleeDiv.querySelectorAll('[class^="props"] i[data-bonus-attachment-title]')]
.map(i => {
return {
title: i.getAttribute('data-bonus-attachment-title') || "",
description: i.getAttribute('data-bonus-attachment-description') || ""
};
})
// filter out empty titles if needed
.filter(b => b.title);
melee = {
weapon: weaponName,
damage: damage,
accuracy: accuracy,
bonus_attachments: bonusAttachments
};
}
const weaponTempDiv = weaponListDiv.querySelector('#weapon_temp');
const weaponTemp = weaponTempDiv?.querySelector('figure[class^="weaponImage"] img')?.alt.trim() || "N/A";
const armorPrefix = getArmorPrefix(armor);
const data = {
Name: nameEl ? `${nameEl.textContent.trim()} [${xid}]` : "N/A",
...(armorPrefix
? { "Full Set": armorPrefix }
: {
Helmet: armor.helmet || "N/A",
Chest: armor.chest || "N/A",
Gloves: armor.gloves || "N/A",
Pants: armor.pants || "N/A",
Boots: armor.boots || "N/A"
}),
Primary: (primary.weapon && primary.weapon.trim() !== "") ? primary : "N/A",
Secondary: (secondary.weapon && secondary.weapon.trim() !== "") ? secondary : "N/A",
Melee: (melee.weapon && melee.weapon.trim() !== "") ? melee : "N/A",
Temp: weaponTemp,
};
console.log("✅ Torn Fight Report Extracted Data:", data);
return data
}
}
const observer = new MutationObserver(
debounce(() => {
const nameElement = document.querySelector('[id^="playername_"]');
const playerElements = document.querySelectorAll('[class^="player___"]');
if (nameElement && playerElements.length >= 2) {
console.log("Checking!")
const indicatorsWrap = playerElements[1].querySelector('div[class^="indicatorsContainer"]');
if (indicatorsWrap && indicatorsWrap.children.length > 0) {
observer.disconnect();
const data = extractData();
insertTable(data);
}
}
}, 1000) // only checks every 1s
);
observer.observe(document.body, { childList: true, subtree: true });
})();