Copy loadouts between slots and quick loadout switcher\
// ==UserScript==
// @name Torn Loadout Copier
// @namespace tornloadoutcopier.zero.nao
// @version 1.1
// @description Copy loadouts between slots and quick loadout switcher\
// @author nao [2669774]
// @match https://www.torn.com/item.php*
// @run-at document-start
// @grant none
// ==/UserScript==
(function () {
'use strict';
let loadoutData = null;
let activeCopy = null;
let selectedSourceID = null;
let selectedTargetID = null;
function getRFC() {
for (const c of document.cookie.split('; ')) {
const [k, v] = c.split('=');
if (k === 'rfc_v') return v;
}
return null;
}
function parseResponse(res) {
if (typeof res === 'string') {
try { return JSON.parse(res); } catch (e) { }
}
return res;
}
function getItemsForSet(setID) {
if (loadoutData?.currentSettings?.currentSetID == setID) return loadoutData.currentItems;
return loadoutData.currentLoadouts[setID]?.items;
}
// Intercept fetch to capture loadout data
let lastDataJson = '';
const _fetch = window.fetch;
window.fetch = async function (...args) {
const res = await _fetch.apply(this, args);
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '';
if (url.includes('sid=itemsLoadouts') && url.includes('step=getEquippedItems')) {
res.clone().text().then(text => {
if (text === lastDataJson) return;
lastDataJson = text;
loadoutData = JSON.parse(text);
refreshUI();
}).catch(() => { });
}
return res;
};
function refreshUI() {
if (!loadoutData) return;
const container = document.querySelector('.lc-container');
if (container) container.remove();
injectAll();
}
function refreshSwitcher() {
const container = document.querySelector('.lc-container');
if (!container) return;
const old = container.querySelector('.lc-switcher');
const next = buildSwitcher();
if (next) {
if (old) container.replaceChild(next, old);
else container.appendChild(next);
}
}
function getSortedSetIDs() {
if (!loadoutData?.currentLoadouts) return [];
return Object.keys(loadoutData.currentLoadouts).sort((a, b) => a - b);
}
function buildActions(sourceSetID, targetSetID) {
const sourceItemsObj = getItemsForSet(sourceSetID);
const targetItemsObj = getItemsForSet(targetSetID);
if (!sourceItemsObj) return [];
const isTargetCurrent = loadoutData?.currentSettings?.currentSetID == targetSetID;
const targetItemsArray = targetItemsObj ? Object.values(targetItemsObj) : [];
const actions = [];
if (!isTargetCurrent) {
actions.push({ type: 'changeLoadout', setID: targetSetID, label: 'Switch to target loadout' });
}
for (const slotKey in sourceItemsObj) {
const src = sourceItemsObj[slotKey];
if (!src?.itemID && !src?.ID) continue;
const srcItemID = src.itemID || src.ID;
const targetItem = targetItemsArray.find(t =>
t.armouryID === src.armouryID ||
(t.itemID === srcItemID && t.name === src.name)
);
if (!targetItem) {
actions.push({
type: 'equipItem',
itemID: srcItemID,
armouryID: src.armouryID,
label: `Equip ${src.name}`
});
}
if (src.currentUpgrades?.length) {
for (const upg of src.currentUpgrades) {
if (targetItem?.currentUpgrades?.some(t => t.upgradeID === upg.upgradeID)) continue;
actions.push({
type: 'reattach',
armouryID: src.armouryID,
upgradeID: upg.upgradeID,
label: `Attach ${upg.title || 'Mod'} → ${src.name}`
});
}
}
if (src.currentAmmoType) {
actions.push({
type: 'changeAmmoType',
armouryID: src.armouryID,
targetAmmoType: src.currentAmmoType,
itemName: src.name,
label: `Sync Ammo → ${src.name}`
});
}
}
return actions;
}
async function executeAction(action) {
const rfc = getRFC();
if (action.type === 'changeLoadout') {
const data = parseResponse(await $.post(`/page.php?sid=itemsLoadouts&step=changeLoadout&setID=${action.setID}&rfcv=${rfc}`));
if (data?.currentItems) loadoutData.currentItems = data.currentItems;
if (data?.currentSettings) loadoutData.currentSettings = data.currentSettings;
return data;
}
if (action.type === 'equipItem') {
return $.post(`/item.php?rfcv=${rfc}`, {
step: 'actionForm',
item_id: action.itemID,
type: '1',
action: 'equip',
item: action.itemID,
id: action.armouryID,
confirm: '1'
});
}
if (action.type === 'reattach') {
return $.post(`/page.php?sid=itemsModsData&step=reattach&armouryID=${action.armouryID}&upgradeID=${action.upgradeID}&replaceUpgradeID=&rfcv=${rfc}`);
}
if (action.type === 'changeAmmoType') {
return $.post(`/page.php?sid=itemsLoadouts&step=changeAmmoType&armouryID=${action.armouryID}&rfcv=${rfc}`);
}
}
// Loadout Switcher
const GLOW_COLORS = {
'glow-red': '#c0392b', 'glow-orange': '#e67e22', 'glow-yellow': '#f1c40f',
'glow-green': '#27ae60', 'glow-blue': '#2980b9', 'glow-purple': '#8e44ad', 'glow-white': '#bdc3c7',
};
const WEAPON_TYPES = new Set(['Primary', 'Secondary', 'Melee', 'Temporary']);
const ARMOR_TYPES = new Set(['Defensive']);
function buildItemRow(item, isWeapon) {
const color = GLOW_COLORS[item?.glowClass] || '#888';
const row = document.createElement('div');
row.style.cssText = `display:flex;flex-direction:column;gap:1px;padding:3px 5px;border-left:2px solid ${color};margin-bottom:3px;`;
const nameLine = document.createElement('div');
nameLine.style.cssText = `font-size:10px;font-weight:bold;color:${color};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;`;
nameLine.textContent = item?.name || '—';
row.appendChild(nameLine);
const toStat = v => { const n = parseFloat(v); return isFinite(n) && n > 0 ? n : 0; };
const parts = [];
if (isWeapon) {
const dmg = toStat(item?.dmg) || toStat(item?.damage);
const acc = toStat(item?.acc) || toStat(item?.accuracy);
const qual = toStat(item?.quality);
if (dmg) parts.push('DMG ' + dmg.toFixed(0));
if (acc) parts.push('ACC ' + acc.toFixed(0));
if (qual) parts.push('Q ' + qual.toFixed(0));
} else {
const arm = toStat(item?.arm);
const qual = toStat(item?.quality);
if (arm) parts.push('MIT ' + arm.toFixed(0));
if (qual) parts.push('Q ' + qual.toFixed(0));
}
if (parts.length) {
const stats = document.createElement('div');
stats.style.cssText = 'font-size:9px;color:#888;';
stats.textContent = parts.join(' ');
row.appendChild(stats);
}
const bonuses = Array.isArray(item?.currentBonuses) ? item.currentBonuses : [];
const bonusText = bonuses
.filter(b => b?.title)
.map(b => b.title + (b.value != null ? ' ' + b.value + '%' : ''))
.join(' · ');
if (bonusText) {
const bonusEl = document.createElement('div');
bonusEl.style.cssText = 'font-size:9px;color:#6b8;';
bonusEl.textContent = bonusText;
row.appendChild(bonusEl);
}
return row;
}
function buildSwitcherCard(setID, isActive, isMinified) {
const loadout = loadoutData.currentLoadouts[setID];
const items = Object.values(getItemsForSet(setID) || {}).filter(i => i?.name);
const weapons = items.filter(i => WEAPON_TYPES.has(i.type2));
const armors = items.filter(i => ARMOR_TYPES.has(i.type2));
const card = document.createElement('div');
card.style.cssText = [
isMinified ? 'min-width:100px;' : 'min-width:140px;max-width:200px;',
'flex:1;',
'background:' + (isActive ? '#1a2a1a' : '#1a1a1a') + ';',
'border:1px solid ' + (isActive ? '#4a7a2a' : '#333') + ';',
'border-radius:5px;padding:6px 7px;cursor:pointer;',
'transition:border-color 0.15s,background 0.15s;font-family:monospace;',
].join('');
card.addEventListener('mouseenter', () => {
if (!isActive && setID !== selectedSourceID && setID !== selectedTargetID) {
card.style.borderColor = '#555';
card.style.background = '#222';
}
});
card.addEventListener('mouseleave', () => {
if (!isActive) {
if (setID === selectedSourceID) card.style.borderColor = '#e67e22';
else if (setID === selectedTargetID) card.style.borderColor = '#2980b9';
else card.style.borderColor = '#333';
card.style.background = '#1a1a1a';
}
});
const header = document.createElement('div');
header.style.cssText = `display:flex;align-items:center;justify-content:space-between;${isMinified ? '' : 'margin-bottom:5px;'}`;
const title = document.createElement('span');
title.style.cssText = 'font-size:11px;font-weight:bold;color:' + (isActive ? '#6b8e23' : '#bbb') + ';letter-spacing:0.5px;text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
title.textContent = loadout?.title || 'Set ' + setID;
header.appendChild(title);
if (isActive) {
const badge = document.createElement('span');
badge.style.cssText = 'font-size:8px;color:#6b8e23;border:1px solid #6b8e23;border-radius:2px;padding:0 3px;margin-left:4px;white-space:nowrap;';
badge.textContent = 'ON';
header.appendChild(badge);
}
if (setID === selectedSourceID) {
const badge = document.createElement('span');
badge.style.cssText = 'font-size:8px;color:#e67e22;border:1px solid #e67e22;border-radius:2px;padding:0 3px;margin-left:4px;white-space:nowrap;font-weight:bold;';
badge.textContent = 'SOURCE ➔';
header.appendChild(badge);
card.style.boxShadow = 'inset 0 0 10px rgba(230, 126, 34, 0.2)';
card.style.borderColor = '#e67e22';
} else if (setID === selectedTargetID) {
const badge = document.createElement('span');
badge.style.cssText = 'font-size:8px;color:#2980b9;border:1px solid #2980b9;border-radius:2px;padding:0 3px;margin-left:4px;white-space:nowrap;font-weight:bold;';
badge.textContent = '➔ TARGET';
header.appendChild(badge);
card.style.boxShadow = 'inset 0 0 10px rgba(41, 128, 185, 0.2)';
card.style.borderColor = '#2980b9';
}
card.appendChild(header);
if (!isMinified) {
if (weapons.length) {
const sec = document.createElement('div');
sec.style.cssText = 'font-size:8px;color:#555;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:2px;';
sec.textContent = 'WEAPONS';
card.appendChild(sec);
weapons.forEach(w => card.appendChild(buildItemRow(w, true)));
}
if (armors.length) {
const sec = document.createElement('div');
sec.style.cssText = 'font-size:8px;color:#555;text-transform:uppercase;letter-spacing:0.5px;margin:4px 0 2px;';
sec.textContent = 'ARMOR';
card.appendChild(sec);
armors.forEach(a => card.appendChild(buildItemRow(a, false)));
}
}
if (!isActive) {
card.addEventListener('click', async () => {
card.style.opacity = '0.5';
card.style.pointerEvents = 'none';
try {
await executeAction({ type: 'changeLoadout', setID });
refreshUI();
} catch (e) {
card.style.opacity = '1';
card.style.pointerEvents = '';
}
});
}
return card;
}
function buildSwitcher() {
if (!loadoutData?.currentLoadouts) return null;
const setIDs = getSortedSetIDs();
if (!setIDs.length) return null;
const isMinified = localStorage.getItem('lc-switcher-minified') === 'true';
const activeSetID = String(loadoutData?.currentSettings?.currentSetID);
const wrap = document.createElement('div');
wrap.className = 'lc-switcher';
wrap.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;padding:10px 15px;margin-bottom:10px;background:#111;border:1px solid #2a2a2a;border-radius:5px;';
const labelRow = document.createElement('div');
labelRow.style.cssText = 'width:100%;display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;';
const label = document.createElement('div');
label.style.cssText = 'font-size:10px;color:#444;font-family:monospace;letter-spacing:1px;text-transform:uppercase;';
label.textContent = 'QUICK SWITCH';
labelRow.appendChild(label);
const toggle = document.createElement('button');
toggle.style.cssText = 'background:none;border:1px solid #333;color:#555;font-size:9px;padding:1px 6px;cursor:pointer;border-radius:3px;font-family:monospace;transition:all 0.2s;';
toggle.textContent = isMinified ? 'EXPAND' : 'MINIFY';
toggle.onmouseover = () => { toggle.style.color = '#888'; toggle.style.borderColor = '#444'; };
toggle.onmouseout = () => { toggle.style.color = '#555'; toggle.style.borderColor = '#333'; };
toggle.onclick = (e) => {
e.preventDefault();
localStorage.setItem('lc-switcher-minified', !isMinified);
refreshUI();
};
labelRow.appendChild(toggle);
wrap.appendChild(labelRow);
const cardsContainer = document.createElement('div');
cardsContainer.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;width:100%;';
for (const sid of setIDs) {
cardsContainer.appendChild(buildSwitcherCard(sid, sid === activeSetID, isMinified));
}
wrap.appendChild(cardsContainer);
return wrap;
}
function buildCopier() {
if (!loadoutData?.currentLoadouts) return null;
const setIDs = getSortedSetIDs();
if (setIDs.length < 2) return null;
const wrap = document.createElement('div');
wrap.style.cssText = 'display:flex;flex-direction:column;padding:10px 15px;margin-bottom:10px;background:#222;border:1px solid #333;border-radius:5px;gap:6px;font-family:monospace;box-shadow:0 2px 4px rgba(0,0,0,0.5);';
const controls = document.createElement('div');
controls.style.cssText = 'display:flex;align-items:center;gap:12px;';
const title = document.createElement('span');
title.textContent = 'LOADOUT COPIER';
title.style.cssText = 'font-size:12px;color:#6b8e23;font-weight:bold;letter-spacing:1px;margin-right:8px;';
const labelFrom = document.createElement('span');
labelFrom.textContent = 'FROM';
labelFrom.style.cssText = 'font-size:11px;color:#777;letter-spacing:0.5px;';
const selectFrom = document.createElement('select');
selectFrom.style.cssText = 'font-size:11px;padding:3px 6px;background:#111;color:#ccc;border:1px solid #444;border-radius:3px;outline:none;text-transform:uppercase;cursor:pointer;';
const labelTo = document.createElement('span');
labelTo.textContent = 'TO';
labelTo.style.cssText = 'font-size:11px;color:#777;letter-spacing:0.5px;';
const selectTo = document.createElement('select');
selectTo.style.cssText = 'font-size:11px;padding:3px 6px;background:#111;color:#ccc;border:1px solid #444;border-radius:3px;outline:none;text-transform:uppercase;cursor:pointer;';
selectFrom.appendChild(new Option('-- SELECT SOURCE --', ''));
selectTo.appendChild(new Option('-- SELECT TARGET --', ''));
for (const sid of setIDs) {
const name = loadoutData.currentLoadouts[sid].title || `Set ${sid}`;
selectFrom.appendChild(new Option(name, sid));
selectTo.appendChild(new Option(name, sid));
}
selectFrom.value = selectedSourceID || '';
selectTo.value = selectedTargetID || '';
const btn = document.createElement('button');
btn.innerHTML = '►';
btn.title = 'Start copy';
btn.disabled = !(selectedSourceID && selectedTargetID);
btn.style.cssText = `display:flex;align-items:center;justify-content:center;font-size:16px;width:34px;height:28px;background:transparent;color:#6b8e23;border:1px solid #6b8e23;border-radius:4px;cursor:pointer;transition:all 0.2s;opacity:${btn.disabled ? '0.4' : '1'};flex-shrink:0;`;
const updateSelection = () => {
selectedSourceID = selectFrom.value;
selectedTargetID = selectTo.value;
if (selectedSourceID && selectedSourceID === selectedTargetID) {
selectTo.value = '';
selectedTargetID = null;
}
btn.disabled = !(selectedSourceID && selectedTargetID);
btn.style.opacity = btn.disabled ? '0.4' : '1';
refreshSwitcher();
};
selectFrom.addEventListener('change', updateSelection);
selectTo.addEventListener('change', updateSelection);
btn.onmouseover = () => { if (!btn.disabled) { btn.style.background = '#6b8e23'; btn.style.color = '#111'; } };
btn.onmouseout = () => { if (!btn.disabled) { btn.style.background = 'transparent'; btn.style.color = '#6b8e23'; } };
const status = document.createElement('div');
status.style.cssText = 'font-size:11px;color:#aaa;min-height:14px;';
const resetCopier = () => {
status.textContent = '';
btn.innerHTML = '►';
btn.title = 'Start copy';
btn.style.borderColor = '#6b8e23';
btn.style.color = '#6b8e23';
selectFrom.disabled = false;
selectTo.disabled = false;
selectFrom.style.opacity = '1';
selectTo.style.opacity = '1';
activeCopy = null;
};
const showDone = () => {
status.textContent = 'DONE AND EQUIPPED';
status.style.color = '#6b8e23';
btn.innerHTML = '↺';
btn.title = 'Reset';
};
const skipCompletedAmmo = () => {
while (activeCopy.index < activeCopy.actions.length) {
const a = activeCopy.actions[activeCopy.index];
if (a.type !== 'changeAmmoType') break;
const matched = Object.values(loadoutData.currentItems || {}).find(i => i.armouryID == a.armouryID);
if (!matched || matched.currentAmmoType != a.targetAmmoType) break;
activeCopy.index++;
}
};
btn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const sourceID = selectFrom.value;
const targetID = selectTo.value;
if (!activeCopy || activeCopy.sourceSetID !== sourceID || activeCopy.targetSetID !== targetID) {
const actions = buildActions(sourceID, targetID);
if (!actions.length) {
status.textContent = 'NO CHANGES DETECTED';
status.style.color = '#aaa';
return;
}
activeCopy = { sourceSetID: sourceID, targetSetID: targetID, actions, index: 0 };
selectFrom.disabled = true;
selectTo.disabled = true;
selectFrom.style.opacity = '0.5';
selectTo.style.opacity = '0.5';
btn.style.borderColor = '#aaa';
btn.style.color = '#aaa';
}
skipCompletedAmmo();
if (activeCopy.index >= activeCopy.actions.length) {
if (btn.title !== 'Reset') { showDone(); return; }
resetCopier();
return;
}
const action = activeCopy.actions[activeCopy.index];
status.textContent = action.label;
status.style.color = '#888';
btn.innerHTML = '⌛';
btn.disabled = true;
try {
const res = await executeAction(action);
if (action.type === 'changeAmmoType') {
if (!action.seenAmmoTypes) action.seenAmmoTypes = new Set();
const data = parseResponse(res);
if (data?.currentAmmoType) {
if (data.currentAmmoType === action.targetAmmoType) {
activeCopy.index++;
} else if (action.seenAmmoTypes.has(data.currentAmmoType)) {
activeCopy.index++;
} else {
action.seenAmmoTypes.add(data.currentAmmoType);
action.label = `Match ammo -> ${data.clips?.title || data.currentAmmoType} -> ${action.itemName}`;
}
} else {
activeCopy.index++;
}
} else {
activeCopy.index++;
}
refreshSwitcher();
if (activeCopy.index >= activeCopy.actions.length) {
showDone();
} else {
const progress = `${activeCopy.index}/${activeCopy.actions.length}`;
status.textContent = `${progress} — ${activeCopy.actions[activeCopy.index].label}`;
status.style.color = '#aaa';
btn.innerHTML = '►';
btn.title = 'Next action';
}
} catch (err) {
status.textContent = 'ERROR - RETRY';
status.style.color = '#d9534f';
btn.innerHTML = '↻';
btn.style.borderColor = '#d9534f';
btn.style.color = '#d9534f';
}
btn.disabled = false;
});
controls.append(title, labelFrom, selectFrom, labelTo, selectTo, btn);
wrap.append(controls, status);
return wrap;
}
function injectAll() {
if (!loadoutData?.currentLoadouts) return;
const target = document.querySelector('.equipped-items-wrap');
if (!target || document.querySelector('.lc-container')) return;
const container = document.createElement('div');
container.className = 'lc-container';
const copier = buildCopier();
const switcher = buildSwitcher();
if (copier) container.appendChild(copier);
if (switcher) container.appendChild(switcher);
target.insertAdjacentElement('beforebegin', container);
}
const observer = new MutationObserver(() => {
if (loadoutData && !document.querySelector('.lc-container') && document.querySelector('.equipped-items-wrap')) {
injectAll();
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
injectAll();
})();