Torn Elimination Filter

Filter players by Status, Level, Attacks, and BSP Stats (Min & Max)

目前為 2025-12-10 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Torn Elimination Filter
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Filter players by Status, Level, Attacks, and BSP Stats (Min & Max)
// @author       Legaci [2100546]
// @match        https://www.torn.com/page.php?sid=competition*
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const STORAGE_KEY = 'torn_elim_filter_v5';

    // --- CSS STYLES ---
    GM_addStyle(`
        #torn-elim-filter-bar {
            background: linear-gradient(180deg, #333 0%, #222 100%);
            border: 1px solid #444;
            border-radius: 8px;
            padding: 12px;
            margin: 10px 0;
            display: flex;
            flex-wrap: wrap;
            gap: 15px;
            align-items: center;
            font-family: 'Arial', sans-serif;
            font-size: 13px;
            color: #ddd;
            box-shadow: 0 2px 5px rgba(0,0,0,0.3);
        }
        #torn-elim-filter-bar .filter-group {
            display: flex;
            align-items: center;
            gap: 8px;
            background: #444;
            padding: 5px 10px;
            border-radius: 4px;
            border: 1px solid #555;
        }
        #torn-elim-filter-bar label {
            font-weight: bold;
            color: #bbb;
            cursor: pointer;
            user-select: none;
        }
        #torn-elim-filter-bar input[type="number"], #torn-elim-filter-bar input[type="text"] {
            background: #222;
            border: 1px solid #555;
            color: #fff;
            padding: 4px;
            border-radius: 3px;
            text-align: center;
        }
        #torn-elim-filter-bar input[type="checkbox"] {
            accent-color: #85c742;
            cursor: pointer;
            width: 14px;
            height: 14px;
        }
        #torn-elim-filter-bar select {
            background: #222;
            border: 1px solid #555;
            color: #fff;
            padding: 4px;
            border-radius: 3px;
        }
        .filter-title {
            text-transform: uppercase;
            font-size: 10px;
            color: #888;
            margin-right: 5px;
        }
    `);

    // --- SELECTORS ---
    const SEL = {
        container: '.virtualContainer___Ft72x',
        row: '.dataGridRow___FAAJF.teamRow___R3ZLF',
        status: '.status___w4nOU',
        level: '.level___GCOaT',
        attacks: '.attacks___IJtzw',
        stats: '.TDup_ColoredStatsInjectionDivWithoutHonorBar .iconStats, .iconStats'
    };

    // --- CONFIGURATION ---
    let config = {
        minLevel: 0,
        maxLevel: 100,
        minAttacks: 0,
        maxAttacks: 99999,
        minStatsInput: '',
        maxStatsInput: '',
        onlyShowOkay: false,
        filterMode: 'opacity'
    };

    // Load saved settings
    const saved = localStorage.getItem(STORAGE_KEY);
    if (saved) {
        try { config = { ...config, ...JSON.parse(saved) }; } catch (e) {}
    }

    function saveConfig() {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
    }

    // --- UTILITY: STAT PARSER ---
    // Converts "2.5t" -> 2,500,000,000,000
    function parseStatString(str) {
        if (!str) return 0;
        str = str.toLowerCase().replace(/,/g, '').trim();
        let multiplier = 1;

        if (str.endsWith('k')) multiplier = 1e3;
        else if (str.endsWith('m')) multiplier = 1e6;
        else if (str.endsWith('b')) multiplier = 1e9;
        else if (str.endsWith('t')) multiplier = 1e12;
        else if (str.endsWith('q')) multiplier = 1e15;

        const num = parseFloat(str);
        return isNaN(num) ? 0 : num * multiplier;
    }

    // --- UI CREATION ---
    function createInterface() {
        const target = document.querySelector('.filterContainer___hjP9P') || document.querySelector('.teamPageWrapper___WTu3D');
        if (!target || document.getElementById('torn-elim-filter-bar')) return;

        const bar = document.createElement('div');
        bar.id = 'torn-elim-filter-bar';

        const isChecked = (val) => val ? 'checked' : '';

        bar.innerHTML = `
            <div class="filter-group">
                <input type="checkbox" id="tef-okay" ${isChecked(config.onlyShowOkay)}>
                <label for="tef-okay" style="color:#85c742">Only Show "Okay"</label>
            </div>

            <div class="filter-group">
                <span class="filter-title">Stats</span>
                <input type="text" id="tef-min-stats" value="${config.minStatsInput}" placeholder="Min (1m)" style="width: 60px;">
                <span>-</span>
                <input type="text" id="tef-max-stats" value="${config.maxStatsInput}" placeholder="Max (5b)" style="width: 60px;">
            </div>

            <div class="filter-group">
                <span class="filter-title">Level</span>
                <input type="number" id="tef-min-lvl" value="${config.minLevel}" min="1" max="100" style="width:40px">
                <span>-</span>
                <input type="number" id="tef-max-lvl" value="${config.maxLevel}" min="1" max="100" style="width:40px">
            </div>

            <div class="filter-group">
                <span class="filter-title">Attacks</span>
                <input type="number" id="tef-min-atk" value="${config.minAttacks}" placeholder="Min" style="width:40px">
                <span>-</span>
                <input type="number" id="tef-max-atk" value="${config.maxAttacks === 99999 ? '' : config.maxAttacks}" placeholder="Max" style="width:50px;">
            </div>

            <div class="filter-group" style="margin-left:auto; background:none; border:none;">
                <select id="tef-mode">
                    <option value="opacity" ${config.filterMode === 'opacity' ? 'selected' : ''}>Dimmed</option>
                    <option value="hide" ${config.filterMode === 'hide' ? 'selected' : ''}>Hidden</option>
                </select>
            </div>
        `;

        target.parentNode.insertBefore(bar, target.nextSibling);
        attachListeners();
    }

    function updateSetting(key, value) {
        config[key] = value;
        saveConfig();
        reapplyFilters();
    }

    function attachListeners() {
        document.getElementById('tef-okay').addEventListener('change', e => updateSetting('onlyShowOkay', e.target.checked));

        // Stats
        document.getElementById('tef-min-stats').addEventListener('input', e => updateSetting('minStatsInput', e.target.value));
        document.getElementById('tef-max-stats').addEventListener('input', e => updateSetting('maxStatsInput', e.target.value));

        // Level
        document.getElementById('tef-min-lvl').addEventListener('input', e => updateSetting('minLevel', parseInt(e.target.value) || 0));
        document.getElementById('tef-max-lvl').addEventListener('input', e => updateSetting('maxLevel', parseInt(e.target.value) || 100));

        // Attacks
        document.getElementById('tef-min-atk').addEventListener('input', e => updateSetting('minAttacks', parseInt(e.target.value) || 0));
        document.getElementById('tef-max-atk').addEventListener('input', e => {
            const val = e.target.value === '' ? 99999 : parseInt(e.target.value);
            updateSetting('maxAttacks', val);
        });

        document.getElementById('tef-mode').addEventListener('change', e => updateSetting('filterMode', e.target.value));
    }

    // --- CORE FILTER LOGIC ---
    function shouldHideRow(row) {
        // 1. STATUS CHECK
        if (config.onlyShowOkay) {
            const statusNode = row.querySelector(SEL.status);
            if (statusNode) {
                const statusText = statusNode.innerText.trim().toLowerCase();
                if (!statusText.includes('okay')) return true; // Hide if NOT okay
            }
        }

        // 2. STATS CHECK (Min & Max)
        // Only run logic if inputs are present
        if (config.minStatsInput || config.maxStatsInput) {
            const statNode = row.querySelector(SEL.stats);

            if (statNode) {
                const playerStats = parseStatString(statNode.innerText);
                const minLimit = parseStatString(config.minStatsInput);
                const maxLimit = parseStatString(config.maxStatsInput);

                // If Min is set and player is below min -> Hide
                if (minLimit > 0 && playerStats < minLimit) return true;

                // If Max is set and player is above max -> Hide
                if (maxLimit > 0 && playerStats > maxLimit) return true;
            } else {
                // Edge case: If filtering by stats but player has NO stats visible (BSP unknown),
                // think ill show them just in case

                // return true;
            }
        }

        // 3. LEVEL CHECK
        const lvlNode = row.querySelector(SEL.level);
        if (lvlNode) {
            const lvl = parseInt(lvlNode.innerText.trim());
            if (lvl < config.minLevel || lvl > config.maxLevel) return true;
        }

        // 4. ATTACKS CHECK
        const atkNode = row.querySelector(SEL.attacks);
        if (atkNode) {
            const attacks = parseInt(atkNode.innerText.replace(/,/g, '').trim()) || 0;
            if (attacks < config.minAttacks || attacks > config.maxAttacks) return true;
        }

        return false;
    }

    function applyVisuals(row, shouldHide) {
        if (shouldHide) {
            if (config.filterMode === 'hide') {
                row.style.display = 'none';
            } else {
                row.style.display = '';
                row.style.opacity = '0.05';
                row.style.pointerEvents = 'none';
                row.style.filter = 'grayscale(100%)';
            }
        } else {
            row.style.display = '';
            row.style.opacity = '1';
            row.style.pointerEvents = 'auto';
            row.style.filter = 'none';
        }
    }

    function processRows(rows) {
        rows.forEach(row => {
            if (row.matches(SEL.row)) {
                applyVisuals(row, shouldHideRow(row));
            }
        });
    }

    function reapplyFilters() {
        const container = document.querySelector(SEL.container);
        if (container) {
            const rows = container.querySelectorAll(SEL.row);
            processRows(rows);
        }
    }

    // --- OBSERVER ---
    const observer = new MutationObserver((mutations) => {
        let newNodes = [];
        mutations.forEach(m => {
            m.addedNodes.forEach(node => {
                if (node.nodeType === 1 && node.matches(SEL.row)) {
                    newNodes.push(node);
                }
            });
        });
        if (newNodes.length > 0) processRows(newNodes);
    });

    // --- INIT ---
    const initInterval = setInterval(() => {
        const container = document.querySelector(SEL.container);
        if (container) {
            clearInterval(initInterval);
            createInterface();
            reapplyFilters();
            observer.observe(container, { childList: true, subtree: true });
        }
    }, 500);

})();