Inventory value summary overlay with sorting and scan tools.
// ==UserScript==
// @name Torn Inventory Value Summary
// @namespace Torn Inventory Value Summary
// @version 1.1
// @description Inventory value summary overlay with sorting and scan tools.
// @author car [3581510]
// @license GNU GPL v3
// @match *.torn.com/item.php*
// @grant none
// Thanks to Creator: Mephiles [2087524], DeKleineKobini [2114440] & bandirao [1936821] for TornTools
// ==/UserScript==
(function () {
'use strict';
const styles = `
#inventory-summary-container {
background-color: #333;
border: 1px solid #000;
border-radius: 5px;
margin: 25px 0 15px 0;
font-family: Arial, sans-serif;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.8);
width: 100%;
}
.tm-header {
background: linear-gradient(to bottom, #3a3a3a 0%, #222 100%);
padding: 12px 15px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
border-bottom: 1px solid #000;
min-height: 44px;
box-sizing: border-box;
gap: 10px;
}
.tm-header h3 {
margin: 0;
color: #fff;
font-size: 15px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.5px;
line-height: 1.2;
flex: 1;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.tm-header span.tm-arrow {
color: #aaa;
font-size: 12px;
padding-right: 5px;
flex-shrink: 0;
}
.tm-content {
background-color: #222;
padding: 15px;
}
.tm-controls {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 10px;
border-bottom: 1px solid #444;
margin-bottom: 10px;
}
.tm-scroll-area {
max-height: 480px;
overflow-y: auto;
scrollbar-width: thin;
}
.tm-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
border-bottom: 1px solid #333;
font-size: 13px;
color: #ccc;
min-height: 42px;
}
.tm-row:hover {
background-color: #2b2b2b;
}
.tm-item-info {
flex-grow: 1;
}
.tm-item-actions {
display: flex;
gap: 12px;
align-items: center;
justify-content: flex-end;
min-width: 220px;
}
.tm-price {
font-family: 'Courier New', Courier, monospace;
color: #85bb65;
font-weight: bold;
min-width: 110px;
text-align: right;
font-size: 14px;
}
.tm-btn {
cursor: pointer;
opacity: 0.7;
transition: transform 0.1s;
font-size: 18px;
user-select: none;
width: 24px;
text-align: center;
}
.tm-btn:hover {
opacity: 1;
transform: scale(1.2);
}
.tm-use { color: #5eb35e; }
.tm-send { color: #66a3ff; }
.tm-donate { color: #d187ff; }
.tm-trash { color: #ff4d4d; }
.tm-footer {
background: #1a1a1a;
padding: 12px 20px;
text-align: right;
border-top: 1px solid #444;
}
#tm-sort, #tm-scan-btn {
background: #111;
color: #fff;
border: 1px solid #444;
padding: 5px 12px;
font-size: 13px;
border-radius: 3px;
cursor: pointer;
}
#tm-scan-btn {
background: #36454F;
font-weight: bold;
}
.hidden { display: none !important; }
.btn-placeholder { width: 24px; }
`;
const styleSheet = document.createElement("style");
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
let lastCount = 0;
let updateTimeout;
function formatCurrency(num) {
return '$' + num.toLocaleString();
}
function triggerNativeAction(itemId, actionClass) {
const nativeItem = document.querySelector(`li[data-item="${itemId}"]`);
if (nativeItem) {
const btn = nativeItem.querySelector(`.${actionClass}`);
if (btn) btn.click();
}
}
function parseInventory() {
const items = [];
document.querySelectorAll('li[data-item]').forEach(el => {
const name = el.getAttribute('data-sort');
const id = el.getAttribute('data-item');
const qty = parseInt(el.getAttribute('data-qty')) || 0;
const priceText = el.querySelector('.tt-item-price span')?.innerText || "";
const unitPrice = parseInt(priceText.replace(/[^0-9]/g, '')) || 0;
if (name && name !== "Unknown Item" && unitPrice > 0) {
items.push({
id,
name,
qty,
totalValue: unitPrice * qty,
canUse: !!el.querySelector('.option-use'),
canSend: !!el.querySelector('.option-send'),
canDonate: !!el.querySelector('.option-donate-faction'),
canDelete: !!el.querySelector('.option-delete')
});
}
});
return items;
}
function manualScan() {
const btn = document.getElementById('tm-scan-btn');
btn.innerText = "Calculating...";
// Small delay so UI updates smoothly
setTimeout(() => {
const items = parseInventory();
if (items.length === 0) {
alert("No items detected. Scroll through your inventory first.");
btn.innerText = "Get Total Inventory Value";
return;
}
const visibleItems = document.querySelectorAll('li[data-item]').length;
if (visibleItems < 50) {
if (!confirm("You may not have scrolled through all items.\nContinue anyway?")) {
btn.innerText = "Get Total Inventory Value";
return;
}
}
refreshUI();
btn.innerText = "Get Total Inventory Value";
}, 300);
}
function refreshUI() {
const mode = document.getElementById('tm-sort')?.value || 'valHigh';
let items = parseInventory();
if (mode === 'valHigh') items.sort((a, b) => b.totalValue - a.totalValue);
if (mode === 'valLow') items.sort((a, b) => a.totalValue - b.totalValue);
if (mode === 'qty') items.sort((a, b) => b.qty - a.qty);
renderUI(items, mode);
}
function renderUI(sortedItems, currentSort) {
let container = document.getElementById('inventory-summary-container');
if (!container) {
const insertionPoint = document.querySelector('.content-wrapper') || document.body;
container = document.createElement('div');
container.id = 'inventory-summary-container';
insertionPoint.prepend(container);
}
const grandTotal = sortedItems.reduce((sum, item) => sum + item.totalValue, 0);
container.innerHTML = `
<div class="tm-header" id="tm-toggle">
<h3>Inventory Value Summary (${sortedItems.length})</h3>
<span class="tm-arrow">▼</span>
</div>
<div class="tm-content" id="tm-body">
<div class="tm-controls">
<select id="tm-sort">
<option value="valHigh" ${currentSort === 'valHigh' ? 'selected' : ''}>Value: High to Low</option>
<option value="valLow" ${currentSort === 'valLow' ? 'selected' : ''}>Value: Low to High</option>
<option value="qty" ${currentSort === 'qty' ? 'selected' : ''}>Quantity</option>
</select>
<button id="tm-scan-btn">Get Total Inventory Value</button>
</div>
<div class="tm-scroll-area">
${sortedItems.map(item => `
<div class="tm-row">
<div class="tm-item-info">
<strong>${item.name}</strong> <span style="color:#888">x${item.qty}</span>
</div>
<div class="tm-item-actions">
${item.canUse ? `<span class="tm-btn tm-use" data-id="${item.id}">🍴</span>` : '<div class="btn-placeholder"></div>'}
${item.canSend ? `<span class="tm-btn tm-send" data-id="${item.id}">✉</span>` : '<div class="btn-placeholder"></div>'}
${item.canDonate ? `<span class="tm-btn tm-donate" data-id="${item.id}">🎁</span>` : '<div class="btn-placeholder"></div>'}
${item.canDelete ? `<span class="tm-btn tm-trash" data-id="${item.id}">🗑</span>` : '<div class="btn-placeholder"></div>'}
<div class="tm-price">${formatCurrency(item.totalValue)}</div>
</div>
</div>
`).join('')}
</div>
</div>
<div class="tm-footer">
<strong style="color:#fff; font-size:14px;">GRAND TOTAL: </strong>
<span style="font-family:monospace; color:#85bb65; font-weight:bold; font-size:22px">
${formatCurrency(grandTotal)}
</span>
</div>
`;
document.querySelectorAll('.tm-use').forEach(b =>
b.onclick = () => triggerNativeAction(b.dataset.id, 'option-use')
);
document.querySelectorAll('.tm-send').forEach(b =>
b.onclick = () => triggerNativeAction(b.dataset.id, 'option-send')
);
document.querySelectorAll('.tm-donate').forEach(b =>
b.onclick = () => triggerNativeAction(b.dataset.id, 'option-donate-faction')
);
document.querySelectorAll('.tm-trash').forEach(b =>
b.onclick = () => triggerNativeAction(b.dataset.id, 'option-delete')
);
document.getElementById('tm-toggle').onclick = () =>
document.getElementById('tm-body').classList.toggle('hidden');
document.getElementById('tm-sort').onchange = refreshUI;
document.getElementById('tm-scan-btn').onclick = manualScan;
}
const observer = new MutationObserver(() => {
const currentCount = document.querySelectorAll('li[data-item]').length;
if (currentCount !== lastCount) {
lastCount = currentCount;
clearTimeout(updateTimeout);
updateTimeout = setTimeout(refreshUI, 300);
}
});
observer.observe(document.body, { childList: true, subtree: true });
refreshUI();
})();