Filter players by Status, Level, Attacks, and BSP Stats (Min & Max)
目前為
// ==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);
})();