Torn Armory Cleaner

Batch disposal cleaner. Fixed for 2026 Layout. Features "Keep Best", "Exclude RW", Smart "Tricky Items" settings.

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

You will need to install an extension such as Tampermonkey to install this 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 Armory Cleaner
// @namespace    http://tampermonkey.net/
// @version      2.4
// @description  Batch disposal cleaner. Fixed for 2026 Layout. Features "Keep Best", "Exclude RW", Smart "Tricky Items" settings.
// @author       LOKaa [2834316]
// @match        https://www.torn.com/factions.php?step=your*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration & State ---
    const STORAGE_KEY_TRICKY = 'TORN_CLEANER_TRICKY_LIST';
    const STORAGE_KEY_MINIMIZED = 'TORN_CLEANER_MINIMIZED';

    // Default list
    const DEFAULT_TRICKY = ["Blowgun"];

    // Load config
    let trickyList = [];
    try {
        trickyList = JSON.parse(localStorage.getItem(STORAGE_KEY_TRICKY));
        if (!Array.isArray(trickyList)) trickyList = DEFAULT_TRICKY;
    } catch (e) {
        trickyList = DEFAULT_TRICKY;
    }

    let isMinimized = localStorage.getItem(STORAGE_KEY_MINIMIZED) === 'true';

    // --- CSS ---
    GM_addStyle(`
        .cleaner-dash {
            background: #1b1b1b;
            border: 1px solid #333;
            border-radius: 8px;
            margin: 15px 0;
            box-shadow: 0 4px 10px rgba(0,0,0,0.5);
            font-family: 'Segoe UI', Roboto, Arial, sans-serif;
            font-size: 13px;
            color: #ddd;
            overflow: hidden;
            display: flex;
            flex-direction: column;
            transition: all 0.3s ease;
        }
        .cleaner-header {
            background: linear-gradient(90deg, #8b0000, #4a0000);
            padding: 8px 15px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-bottom: 1px solid #500;
        }
        .cleaner-title { font-weight: 700; font-size: 14px; color: #fff; text-transform: uppercase; letter-spacing: 1px; display: flex; align-items: center; gap: 10px; }
        .cleaner-header-controls { display: flex; gap: 10px; }
        .cleaner-icon-btn { cursor: pointer; color: #ffcccc; font-size: 16px; opacity: 0.8; transition: 0.2s; background: none; border: none; }
        .cleaner-icon-btn:hover { color: #fff; opacity: 1; }

        .cleaner-content {
            padding: 15px;
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
            align-items: center;
            transition: max-height 0.3s ease-out;
            max-height: 500px;
        }
        .cleaner-content.minimized {
            max-height: 0;
            padding: 0 15px;
            overflow: hidden;
        }

        .cleaner-group { display: flex; flex-direction: column; gap: 5px; flex: 1 1 200px; }
        .cleaner-label { font-size: 11px; color: #888; font-weight: 600; text-transform: uppercase; }
        .cleaner-select { background: #2a2a2a; border: 1px solid #444; color: #fff; padding: 8px; border-radius: 4px; width: 100%; outline: none; }
        .cleaner-checks { display: flex; gap: 15px; align-items: center; padding-top: 5px; }
        .cleaner-chk-label { display: flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; font-size: 12px; }
        .cleaner-chk-label input { accent-color: #8b0000; width: 16px; height: 16px; }

        .cleaner-actions {
            display: flex;
            gap: 8px;
            flex-wrap: wrap;
            width: 100%;
            border-top: 1px solid #333;
            padding-top: 15px;
            margin-top: 5px;
        }
        .cleaner-btn {
            flex: 1;
            padding: 10px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-weight: 700;
            text-transform: uppercase;
            font-size: 11px;
            transition: all 0.2s;
            text-align: center;
            color: #fff;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 5px;
        }
        .btn-select { background: #444; border: 1px solid #555; }
        .btn-select:hover { background: #555; border-color: #777; }
        .btn-trash { background: rgba(255, 68, 68, 0.15); border: 1px solid #d32f2f; color: #ff5252; }
        .btn-trash:hover { background: #d32f2f; color: #fff; box-shadow: 0 0 10px rgba(211, 47, 47, 0.4); }
        .btn-clear { background: transparent; border: 1px solid #444; color: #888; flex: 0 0 auto; }
        .btn-clear:hover { color: #ddd; border-color: #666; }
        .btn-scroll-give { background: #007bff; border: 1px solid #0056b3; flex: 0 0 50px; font-size: 16px; }
        .btn-scroll-give:hover { background: #0056b3; }

        .cleaner-stats { background: #111; padding: 4px 15px; font-size: 11px; color: #666; text-align: right; border-top: 1px solid #222; }

        /* Settings Modal */
        .cleaner-modal {
            display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.8); z-index: 99999; align-items: center; justify-content: center;
        }
        .cleaner-modal-box {
            background: #222; border: 1px solid #444; padding: 20px; border-radius: 8px; width: 400px;
            color: #ddd; box-shadow: 0 0 20px rgba(0,0,0,0.8);
        }
        .cleaner-modal h3 { margin-top: 0; border-bottom: 1px solid #444; padding-bottom: 10px; color: #fff; }
        .cleaner-modal textarea {
            width: 100%; height: 150px; background: #111; border: 1px solid #444; color: #0f0;
            font-family: monospace; padding: 10px; margin: 10px 0; resize: vertical; outline: none;
        }
        .cleaner-modal-btns { display: flex; gap: 10px; justify-content: flex-end; }
        .cleaner-help { font-size: 11px; color: #888; margin-bottom: 10px; line-height: 1.4; }

        @media (max-width: 600px) {
            .cleaner-checks { width: 100%; justify-content: space-between; }
            .cleaner-btn { font-size: 10px; padding: 12px 5px; }
        }
    `);

    // --- Helpers ---

    // FIX: Updated to look for the currently visible tab panel using aria-hidden
    function getActiveTabSelector() {
        const activeTab = document.querySelector('#faction-armoury-tabs > .ui-tabs-panel[aria-hidden="false"]');
        if (activeTab) {
            // We return a CSS selector string that targets this specific element
            // We use the ID but escape the special chars, or just use the aria selector which is safer
            return '#faction-armoury-tabs > .ui-tabs-panel[aria-hidden="false"]';
        }
        return null;
    }

    // --- Core Logic ---

    function scanItems(containerSelector) {
        const container = document.querySelector(containerSelector);
        if (!container) return [];

        const items = [];
        const rows = container.querySelectorAll('ul.item-list > li');

        rows.forEach((li, index) => {
            if (li.classList.contains('ts-placeholder')) return;

            // Name Parsing
            const nameEl = li.querySelector('.name');
            if (!nameEl) return;
            const rawName = nameEl.textContent.trim();
            const name = rawName.split(' x')[0];

            // Loan Status
            const loanLink = li.querySelector('.loaned a');
            const isLoaned = !!loanLink;

            // RW / Quality Logic
            let isRW = false;
            let isTricky = trickyList.includes(name);

            // 1. Check Visual Glow (Gold/Orange/Red is ALWAYS special/RW)
            // FIX: The glow class is now on the IMG tag, not the .img-wrap
            const imgEl = li.querySelector('.img-wrap img');
            if (imgEl) {
                if (imgEl.classList.contains('glow-yellow') ||
                    imgEl.classList.contains('glow-orange') ||
                    imgEl.classList.contains('glow-red')) {
                    isRW = true;
                }
            }

            // 2. Check Bonuses ONLY if it's not already marked as RW by color
            // And ONLY if it is not in the Tricky List (Blowgun etc)
            if (!isRW && !isTricky) {
                const bonuses = li.querySelectorAll('ul.bonuses i[title]');
                let hasBonuses = false;
                bonuses.forEach(b => {
                    if (!b.getAttribute('title').includes('blank')) hasBonuses = true;
                });
                if (hasBonuses) isRW = true;
            }

            // Checkbox
            const checkbox = li.querySelector('input[type="checkbox"]');

            items.push({
                index: index,
                element: li,
                name: name,
                isLoaned: isLoaned,
                isRW: isRW,
                checkbox: checkbox
            });
        });

        return items;
    }

    function clearSelections(items) {
        items.forEach(item => {
            if (item.checkbox && item.checkbox.checked) item.checkbox.click();
        });
    }

    function applySelection(targetName, keepBest, excludeRW) {
        const selector = getActiveTabSelector();
        if (!selector) return;

        const items = scanItems(selector);
        clearSelections(items);

        let count = 0;
        const grouped = {};

        // Group items by name
        items.forEach(item => {
            if (!grouped[item.name]) grouped[item.name] = [];
            grouped[item.name].push(item);
        });

        for (const [itemName, groupItems] of Object.entries(grouped)) {
            // Filter by Name
            if (targetName !== 'ALL' && itemName !== targetName) continue;

            // Filter: Valid targets (not loaned)
            let validItems = groupItems.filter(i => !i.isLoaned);

            // Filter: Exclude RW
            if (excludeRW) {
                validItems = validItems.filter(i => !i.isRW);
            }

            // Filter: Keep Best (First one in the list is always best stats in Torn)
            if (keepBest && validItems.length > 0) {
                validItems.shift();
            }

            // Select
            validItems.forEach(item => {
                if (item.checkbox && !item.checkbox.checked && !item.checkbox.disabled) {
                    item.checkbox.click();
                    count++;
                }
            });
        }

        updateStatus(`Selected ${count} items.`);
    }

    function updateStatus(msg) {
        const el = document.getElementById('cleaner-status-text');
        if (el) el.textContent = msg;
    }

    function scrollToGiveBar(parentSelector) {
        const parent = document.querySelector(parentSelector);
        if (parent) {
            const giveBar = parent.querySelector('.give-all');
            if (giveBar) {
                giveBar.scrollIntoView({ behavior: 'smooth', block: 'center' });
            } else {
                alert("Give bar not visible yet. Please select at least one item first.");
            }
        }
    }

    // --- UI Construction ---

    function buildDashboard() {
        const selector = getActiveTabSelector();
        if (!selector) return;
        const container = document.querySelector(selector);

        if (document.getElementById('cleaner-dash')) return; // Already exists

        const listHeader = container.querySelector('ul.list-title');
        if (!listHeader) return;

        // Dropdown Options
        const items = scanItems(selector);
        const uniqueNames = [...new Set(items.map(i => i.name))].sort();
        let optionsHtml = `<option value="ALL">-- ALL ITEMS --</option>`;
        uniqueNames.forEach(n => {
            optionsHtml += `<option value="${n}">${n}</option>`;
        });

        // Dashboard HTML
        const dash = document.createElement('div');
        dash.id = 'cleaner-dash';
        dash.className = 'cleaner-dash';
        dash.innerHTML = `
            <div class="cleaner-header">
                <div class="cleaner-title">
                    <span style="font-size:16px;">🧹</span> Armory Cleaner
                </div>
                <div class="cleaner-header-controls">
                    <span id="cleaner-status-text" class="cleaner-status" style="margin-right:10px; color:#aaa;">Ready</span>
                    <button id="btn-settings" class="cleaner-icon-btn" title="Tricky Items Settings">⚙️</button>
                    <button id="btn-minimize" class="cleaner-icon-btn" title="Toggle Dashboard">${isMinimized ? '➕' : '➖'}</button>
                </div>
            </div>

            <div class="cleaner-content ${isMinimized ? 'minimized' : ''}">
                <div class="cleaner-group" style="flex: 2;">
                    <span class="cleaner-label">Target Item Type</span>
                    <select id="cleaner-target" class="cleaner-select">${optionsHtml}</select>
                </div>

                <div class="cleaner-group" style="flex: 2;">
                    <span class="cleaner-label">Filters</span>
                    <div class="cleaner-checks">
                        <label class="cleaner-chk-label" title="Keep the 1st item of a group (Best stats)">
                            <input type="checkbox" id="cleaner-keep-best" checked> Keep Best
                        </label>
                        <label class="cleaner-chk-label" title="Ignore Yellow/Orange/Red items">
                            <input type="checkbox" id="cleaner-no-rw" checked> Exclude RW
                        </label>
                    </div>
                </div>

                <div class="cleaner-actions">
                    <button id="btn-select-target" class="cleaner-btn btn-trash">Select Target</button>
                    <button id="btn-select-trash" class="cleaner-btn btn-select" title="Target=All, KeepBest=Yes, NoRW=Yes">Select All Trash</button>
                    <button id="btn-clear-all" class="cleaner-btn btn-clear">Clear</button>
                    <button id="btn-scroll-down" class="cleaner-btn btn-scroll-give" title="Scroll to Give Button">⬇</button>
                </div>
            </div>

            <div class="cleaner-stats">
                Found ${items.length} items. Tricky List: ${trickyList.length} items.
            </div>

            <div id="cleaner-settings-modal" class="cleaner-modal">
                <div class="cleaner-modal-box">
                    <h3>Tricky Items Whitelist</h3>
                    <div class="cleaner-help">
                        LOKaa [2834316] </br>
                        -----------------------------</br>
                        Add items here (standard JSON format preferred).<br>
                        Items in this list will be treated as REGULAR items even if they have bonuses.<br>
                        <i>Example:</i><br>
                        <code style="color:#666">
                        [<br>
                          "Blowgun",<br>
                          "Trout"<br>
                        ]
                        </code>
                    </div>
                    <textarea id="tricky-input" spellcheck="false"></textarea>
                    <div class="cleaner-modal-btns">
                        <button id="btn-save-tricky" class="cleaner-btn btn-select">Save & Close</button>
                        <button id="btn-cancel-tricky" class="cleaner-btn btn-clear">Cancel</button>
                    </div>
                </div>
            </div>
        `;

        container.insertBefore(dash, listHeader);

        // --- Event Binding ---

        // Minimize Toggle
        document.getElementById('btn-minimize').addEventListener('click', (e) => {
            const content = dash.querySelector('.cleaner-content');
            isMinimized = !isMinimized;
            localStorage.setItem(STORAGE_KEY_MINIMIZED, isMinimized);
            content.classList.toggle('minimized', isMinimized);
            e.target.textContent = isMinimized ? '➕' : '➖';
        });

        // Settings Modal Controls
        const modal = document.getElementById('cleaner-settings-modal');
        const txtArea = document.getElementById('tricky-input');

        document.getElementById('btn-settings').addEventListener('click', () => {
            // Display neatly formatted JSON
            txtArea.value = JSON.stringify(trickyList, null, 2);
            modal.style.display = 'flex';
        });

        document.getElementById('btn-cancel-tricky').addEventListener('click', () => {
            modal.style.display = 'none';
        });

        // -- Smart Save Handler --
        document.getElementById('btn-save-tricky').addEventListener('click', () => {
            let raw = txtArea.value.trim();
            let parsed = [];

            try {
                // Attempt 1: Parse strict JSON
                parsed = JSON.parse(raw);
            } catch (e) {
                // Attempt 2: Smart Recovery
                parsed = raw.split('\n')
                    .map(line => line.replace(/[\[\]",]/g, '').trim())
                    .filter(line => line.length > 0);
            }

            if (Array.isArray(parsed)) {
                // Remove duplicates and save
                trickyList = [...new Set(parsed)];
                localStorage.setItem(STORAGE_KEY_TRICKY, JSON.stringify(trickyList));
                modal.style.display = 'none';
                updateStatus('Settings Saved. Refreshing...');
                setTimeout(() => { dash.remove(); buildDashboard(); }, 200);
            } else {
                alert("Could not interpret list. Please verify format.");
            }
        });

        // Actions
        document.getElementById('btn-select-target').addEventListener('click', (e) => {
            e.preventDefault();
            const target = document.getElementById('cleaner-target').value;
            const keepBest = document.getElementById('cleaner-keep-best').checked;
            const excludeRW = document.getElementById('cleaner-no-rw').checked;
            applySelection(target, keepBest, excludeRW);
        });

        document.getElementById('btn-select-trash').addEventListener('click', (e) => {
            e.preventDefault();
            applySelection('ALL', true, true);
        });

        document.getElementById('btn-clear-all').addEventListener('click', (e) => {
            e.preventDefault();
            clearSelections(scanItems(selector));
            updateStatus('Cleared.');
        });

        document.getElementById('btn-scroll-down').addEventListener('click', (e) => {
            e.preventDefault();
            scrollToGiveBar(selector);
        });
    }

    // --- Observer ---
    let debounce = null;
    const observer = new MutationObserver((mutations) => {
        const isInternal = mutations.every(m => m.target.closest('#cleaner-dash'));
        if (isInternal) return;

        if (debounce) clearTimeout(debounce);
        debounce = setTimeout(() => {
            const selector = getActiveTabSelector();
            if (selector) {
                const container = document.querySelector(selector);
                const dash = document.getElementById('cleaner-dash');
                // Check if dash is missing or in wrong container
                if (!dash || (container && !container.contains(dash))) {
                    if (dash) dash.remove();
                    buildDashboard();
                }
            }
        }, 300);
    });

    function init() {
        const target = document.getElementById('faction-armoury-tabs');
        if (target) {
            observer.observe(target, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'aria-expanded', 'aria-hidden'] });
            buildDashboard();
        } else {
            setTimeout(init, 500);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();