Torn Loadout Copier

Copy loadouts between slots and quick loadout switcher\

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==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();

})();