Torn Loadout Copier

Copy loadouts between slots and quick loadout switcher\

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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 = '&#9658;';
        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 = '&#9658;';
            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 = '&#8634;';
            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 = '&#8987;';
            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 = '&#9658;';
                    btn.title = 'Next action';
                }
            } catch (err) {
                status.textContent = 'ERROR - RETRY';
                status.style.color = '#d9534f';
                btn.innerHTML = '&#8635;';
                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();

})();