// ==UserScript==
// @name Torn Crime Scenario Filter with Level and Slot Sorting
// @namespace http://tampermonkey.net/
// @version 1.6.0
// @description Adds filters to show/hide crime scenario cards by level and sorts by filled slots on the recruiting tab
// @match https://www.torn.com/factions.php*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Function to check if we're on the correct page
function isCorrectPage() {
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
const hash = url.hash;
return params.get('step') === 'your' &&
params.get('type') === '12' &&
hash.includes('#/tab=crimes');
}
// Constants for localStorage keys
const STORAGE_KEYS = {
SHOW: 'tornCrimeFilter_show',
LEVEL: 'tornCrimeFilter_level',
POSITION_X: 'tornCrimeFilter_posX',
POSITION_Y: 'tornCrimeFilter_posY',
IS_COLLAPSED: 'tornCrimeFilter_collapsed'
};
// Default settings
const DEFAULT_SETTINGS = {
show: true,
level: 1,
positionX: 0,
positionY: 0,
isCollapsed: false
};
// Load saved settings or use defaults
function loadSettings() {
return {
show: localStorage.getItem(STORAGE_KEYS.SHOW) !== 'false',
level: parseInt(localStorage.getItem(STORAGE_KEYS.LEVEL)) || DEFAULT_SETTINGS.level,
positionX: parseInt(localStorage.getItem(STORAGE_KEYS.POSITION_X)) || DEFAULT_SETTINGS.positionX,
positionY: parseInt(localStorage.getItem(STORAGE_KEYS.POSITION_Y)) || DEFAULT_SETTINGS.positionY,
isCollapsed: localStorage.getItem(STORAGE_KEYS.IS_COLLAPSED) === 'true'
};
}
// Save settings to localStorage
function saveSettings(settings) {
Object.entries(settings).forEach(([key, value]) => {
localStorage.setItem(STORAGE_KEYS[key.toUpperCase()], value);
});
}
// Function to get the number of filled slots in a crime card
function getFilledSlots(card) {
const slots = card.querySelectorAll('.memberSlot___WZ6OB');
let filledCount = 0;
slots.forEach(slot => {
if (slot.querySelector('.avatar___V1YqY')) {
filledCount++;
}
});
return filledCount;
}
// Function to sort crime cards by filled slots
function sortCrimeCardsBySlots() {
const crimesContainer = document.querySelector('.scenariosWrapper___FUUuh');
if (!crimesContainer) return;
const cards = Array.from(crimesContainer.querySelectorAll('.wrapper___U2Ap7'));
// Sort cards by number of filled slots (descending)
cards.sort((a, b) => {
const slotsA = getFilledSlots(a);
const slotsB = getFilledSlots(b);
return slotsB - slotsA;
});
// Reorder cards in the DOM
cards.forEach(card => crimesContainer.appendChild(card));
}
// Function to check if Recruiting tab is selected
function isRecruitingTabActive() {
const recruitingButton = document.querySelector('.buttonsContainer___aClaa button.active___ImR61');
return recruitingButton && recruitingButton.textContent.includes('Recruiting');
}
// Ensure position is within viewport bounds
function keepInBounds(element) {
const rect = element.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let newX = parseInt(element.style.transform.split('(')[1]) || 0;
let newY = parseInt(element.style.transform.split(',')[1]) || 0;
if (rect.right > viewportWidth) {
newX = viewportWidth - rect.width;
}
if (rect.left < 0) {
newX = 0;
}
if (rect.bottom > viewportHeight) {
newY = viewportHeight - rect.height;
}
if (rect.top < 0) {
newY = 0;
}
return { x: newX, y: newY };
}
function updateFilters() {
const showCheckbox = document.getElementById('crimeFilterCheckbox');
const levelInput = document.getElementById('minCrimeLevelInput');
const shouldShow = showCheckbox.checked;
const minLevel = parseInt(levelInput.value, 10) || 1;
// Save filter settings
saveSettings({
show: shouldShow,
level: minLevel
});
// Update status text
const statusEl = document.getElementById('filterStatus');
let visibleCount = 0;
let totalCount = 0;
document.querySelectorAll('.wrapper___U2Ap7').forEach(card => {
const levelEl = card.querySelector('.level___sBl49');
totalCount++;
if (levelEl) {
const text = levelEl.textContent.trim();
const parts = text.split('/');
const level = parseInt(parts[0], 10) || 0;
if (!shouldShow || level < minLevel) {
card.style.display = 'none';
} else {
card.style.display = '';
visibleCount++;
}
} else {
card.style.display = shouldShow ? '' : 'none';
if (shouldShow) visibleCount++;
}
});
statusEl.textContent = `Showing ${visibleCount} of ${totalCount} crimes`;
// Sort cards if on Recruiting tab
if (isRecruitingTabActive()) {
sortCrimeCardsBySlots();
}
}
// Function to observe tab changes
function observeTabChanges() {
const tabContainer = document.querySelector('.buttonsContainer___aClaa');
if (!tabContainer) return;
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
if (isRecruitingTabActive()) {
sortCrimeCardsBySlots();
}
}
});
});
observer.observe(tabContainer, {
subtree: true,
attributes: true,
attributeFilter: ['class']
});
}
// Main initialization function
function initializeFilter() {
if (!isCorrectPage()) return;
const crimesContainer = document.getElementById('faction-crimes');
if (!crimesContainer) return;
// Load saved settings
const settings = loadSettings();
// Create filter UI container
const filterDiv = document.createElement('div');
Object.assign(filterDiv.style, {
position: 'fixed',
top: '10px',
right: '10px',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
color: '#fff',
padding: '12px',
zIndex: '10000',
borderRadius: '8px',
fontFamily: 'Arial, sans-serif',
fontSize: '14px',
boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
display: 'flex',
flexDirection: 'column',
gap: '8px',
transition: 'transform 0.3s ease'
});
// Create header with title and collapse button
const headerDiv = document.createElement('div');
headerDiv.style.display = 'flex';
headerDiv.style.justifyContent = 'space-between';
headerDiv.style.alignItems = 'center';
headerDiv.style.marginBottom = '8px';
headerDiv.style.cursor = 'move';
const titleSpan = document.createElement('span');
titleSpan.textContent = 'Crime Filter';
titleSpan.style.fontWeight = 'bold';
const collapseButton = document.createElement('button');
collapseButton.textContent = settings.isCollapsed ? '▼' : '▲';
collapseButton.style.background = 'none';
collapseButton.style.border = 'none';
collapseButton.style.color = '#fff';
collapseButton.style.cursor = 'pointer';
collapseButton.style.padding = '0 5px';
// Create content container
const contentDiv = document.createElement('div');
contentDiv.style.display = settings.isCollapsed ? 'none' : 'block';
// Create controls container
const controlsDiv = document.createElement('div');
controlsDiv.style.display = 'flex';
controlsDiv.style.alignItems = 'center';
controlsDiv.style.gap = '10px';
// Create checkbox group
const checkboxGroup = document.createElement('div');
checkboxGroup.style.display = 'flex';
checkboxGroup.style.alignItems = 'center';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = 'crimeFilterCheckbox';
checkbox.checked = settings.show;
checkbox.style.marginRight = '5px';
const checkboxLabel = document.createElement('label');
checkboxLabel.htmlFor = 'crimeFilterCheckbox';
checkboxLabel.textContent = 'Show Crime Cards';
// Create level input group
const levelGroup = document.createElement('div');
levelGroup.style.display = 'flex';
levelGroup.style.alignItems = 'center';
const levelLabel = document.createElement('label');
levelLabel.htmlFor = 'minCrimeLevelInput';
levelLabel.textContent = 'Min Level:';
const levelInput = document.createElement('input');
levelInput.type = 'number';
levelInput.id = 'minCrimeLevelInput';
levelInput.min = '1';
levelInput.max = '10';
levelInput.value = settings.level;
levelInput.style.width = '45px';
levelInput.style.marginLeft = '5px';
levelInput.style.padding = '2px 4px';
// Create status text
const statusEl = document.createElement('div');
statusEl.id = 'filterStatus';
statusEl.style.fontSize = '12px';
statusEl.style.color = '#aaa';
statusEl.style.marginTop = '4px';
// Assemble the UI
headerDiv.appendChild(titleSpan);
headerDiv.appendChild(collapseButton);
checkboxGroup.appendChild(checkbox);
checkboxGroup.appendChild(checkboxLabel);
levelGroup.appendChild(levelLabel);
levelGroup.appendChild(levelInput);
controlsDiv.appendChild(checkboxGroup);
controlsDiv.appendChild(levelGroup);
contentDiv.appendChild(controlsDiv);
contentDiv.appendChild(statusEl);
filterDiv.appendChild(headerDiv);
filterDiv.appendChild(contentDiv);
document.body.appendChild(filterDiv);
// Set initial position from saved settings
filterDiv.style.transform = `translate(${settings.positionX}px, ${settings.positionY}px)`;
// Collapse button functionality
collapseButton.addEventListener('click', (e) => {
e.stopPropagation();
settings.isCollapsed = !settings.isCollapsed;
contentDiv.style.display = settings.isCollapsed ? 'none' : 'block';
collapseButton.textContent = settings.isCollapsed ? '▼' : '▲';
saveSettings({ isCollapsed: settings.isCollapsed });
});
// Add event listeners for filters
checkbox.addEventListener('change', updateFilters);
levelInput.addEventListener('input', updateFilters);
levelInput.addEventListener('change', updateFilters);
// Make the filter div draggable
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = settings.positionX;
let yOffset = settings.positionY;
function dragStart(e) {
if (e.target === headerDiv || e.target === titleSpan) {
isDragging = true;
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
}
}
function drag(e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
filterDiv.style.transform = `translate(${currentX}px, ${currentY}px)`;
}
}
function dragEnd() {
if (isDragging) {
isDragging = false;
// Keep the box in bounds
const bounds = keepInBounds(filterDiv);
xOffset = bounds.x;
yOffset = bounds.y;
filterDiv.style.transform = `translate(${xOffset}px, ${yOffset}px)`;
// Save the new position
saveSettings({
positionX: xOffset,
positionY: yOffset
});
}
}
headerDiv.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
// Handle window resize to keep the box in bounds
window.addEventListener('resize', () => {
const bounds = keepInBounds(filterDiv);
xOffset = bounds.x;
yOffset = bounds.y;
filterDiv.style.transform = `translate(${xOffset}px, ${yOffset}px)`;
saveSettings({
positionX: xOffset,
positionY: yOffset
});
});
// Add tab change observer
observeTabChanges();
// Initial filter application and sorting
updateFilters();
// Set up observer for dynamic content
const observer = new MutationObserver(() => {
updateFilters();
if (isRecruitingTabActive()) {
sortCrimeCardsBySlots();
}
});
observer.observe(crimesContainer, { childList: true, subtree: true });
}
// Listen for URL changes (for single-page application support)
let lastUrl = location.href;
new MutationObserver(() => {
const url = location.href;
if (url !== lastUrl) {
lastUrl = url;
// Remove existing filter if present
const existingFilter = document.querySelector('#crimeFilterCheckbox')?.closest('div[style*="position: fixed"]');
if (existingFilter) {
existingFilter.remove();
}
// Initialize filter if we're on the correct page
initializeFilter();
}
}).observe(document, { subtree: true, childList: true });
// Initial load
window.addEventListener('load', initializeFilter);
})();