// ==UserScript==
// @name Torn Crimes Card Skimming Extended
// @namespace https://github.com/SOLiNARY
// @version 0.5.6
// @description Sorts all installed card skimmers by location, time installed, score or cards skimmed. Adds card/hour stat. Remembers your choice.
// @author Ramin Quluzade, Silmaril [2665762]
// @license MIT License
// @match https://www.torn.com/loader.php?sid=crimes*
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant none
// ==/UserScript==
(function() {
'use strict';
const sortBy = {
"Location": 10,
"TimeInstalled": 20,
"CardsSkimmed": 30,
"Score": 40
};
const sortDirection = {
"Ascending": 1,
"Descending": -1
}
const viewPortWidthPx = window.innerWidth;
const isMobileView = viewPortWidthPx <= 784;
let currentSortBy = localStorage.getItem("silmaril-torn-crimes-card-skimming-sorting-by") ?? sortBy.Location;
currentSortBy = parseInt(currentSortBy);
let currentSortDirection = localStorage.getItem("silmaril-torn-crimes-card-skimming-sorting-direction") ?? sortDirection.Descending;
currentSortDirection = parseInt(currentSortDirection);
const targetNode = document.querySelector("div.crimes-app");
const config = { childList: true, subtree: true };
const observer = new MutationObserver((mutationsList, observer) => {
const divs = document.querySelectorAll("div[class*=currentCrime___]");
for (const mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.target.className == 'crime-root cardskimming-root') {
divs.forEach((div) => {
div.addEventListener("click", function (event) {
if (event.target.matches("div[class*=topSection___] div[class*=crimeBanner___] div[class*=crimeSliderArrowButtons___] button[class*=arrowButton___]")) {
observer.observe(targetNode, config);
}
if (event.target.matches("div[class*=crimeOptionGroup___]:not([class*=firstGroup___]) div.silmaril-crimes-card-skimming-sorting")) {
let sortName = event.target.getAttribute("data-sort-name");
let newSortBy = sortBy[sortName];
let newSortDirection = newSortBy === currentSortBy ? currentSortDirection * -1 : currentSortDirection;
sortChildElements(mutation.target, newSortBy, newSortDirection);
currentSortBy = newSortBy;
currentSortDirection = newSortDirection;
localStorage.setItem("silmaril-torn-crimes-card-skimming-sorting-by", newSortBy);
localStorage.setItem("silmaril-torn-crimes-card-skimming-sorting-direction", newSortDirection);
}
});
});
addHeader(mutation.target);
sortChildElements(mutation.target, currentSortBy, currentSortDirection);
observer.disconnect();
break;
}
}
});
observer.observe(targetNode, config);
// Function to sort child elements
function sortChildElements(element, sortByProperty, sortDirection) {
const parentElement = element.querySelector('[class*=crimeOptionGroup___]:not([class*=firstGroup___])');
const childElements = Array.from(parentElement.querySelectorAll('[class*=crimeOption___]:not(.silmaril-card-skimming-header)'));
let locationStats = {};
// Append sorted elements back to the parent element
childElements.forEach(element => {
let cardsSkimmed = element.querySelector('[class*=crimeOptionSection___][class*=statusSection___] [class*=statusCards___]').textContent;
let hoursElapsed = parseVerbalTimestamp(element.querySelector(`[class*=crimeOptionSection___]${isMobileView ? '[class*=tabletMainSection___] div[class*=timeActive___]' : '[class*=timeSection___]'}`).textContent) / 3600;
let locationDiv = element.querySelector('[class*=crimeOptionSection___][class*=flexGrow___]');
let location = null;
let locationText = locationDiv.innerText;
let newLineIdx = locationText.indexOf('\n');
if (newLineIdx >= 0){
location = locationText.substring(0, newLineIdx);
} else {
location = locationText;
}
if (element.querySelector('div.stats') === null) {
if (isMobileView){
const statsDivNew = document.createElement('div');
statsDivNew.className = 'stats';
statsDivNew.style.fontSize = '.6rem';
locationDiv.appendChild(statsDivNew);
} else {
const delimiter = document.createElement('div');
delimiter.className = 'sectionDelimiter___NpsSC';
const statsDivNew = document.createElement('div');
statsDivNew.className = 'crimeOptionSection___hslpu stats';
locationDiv.outerHTML += delimiter.outerHTML + statsDivNew.outerHTML;
}
}
let statsDiv = element.querySelector('div.stats');
let statsScore = (cardsSkimmed / hoursElapsed).toFixed(2);
statsDiv.textContent = `${statsScore} card/hour`;
element.setAttribute('data-score', parseFloat(statsScore).toFixed(2));
// Add stats to the locationStats object
if (!locationStats[location]) {
locationStats[location] = {
totalScore: 0,
totalCount: 0
};
}
locationStats[location].totalScore += parseFloat(statsScore);
locationStats[location].totalCount++;
});
// Sort card skimmers based on the filter
switch (sortByProperty){
case sortBy.Location:
childElements.sort((a, b) => {
const aValue = a.querySelector('[class*=crimeOptionSection___][class*=flexGrow___]').textContent;
const bValue = b.querySelector('[class*=crimeOptionSection___][class*=flexGrow___]').textContent;
return aValue.localeCompare(bValue) * sortDirection;
});
break;
case sortBy.TimeInstalled:
if (isMobileView) {
childElements.sort((a, b) => {
const aValue = parseVerbalTimestamp(a.querySelector('[class*=crimeOptionSection___][class*=tabletMainSection___] div[class*=timeActive___]').textContent);
const bValue = parseVerbalTimestamp(b.querySelector('[class*=crimeOptionSection___][class*=tabletMainSection___] div[class*=timeActive___]').textContent);
return (aValue < bValue ? -1 : aValue > bValue ? 1 : 0) * sortDirection;
});
} else {
childElements.sort((a, b) => {
const aValue = parseVerbalTimestamp(a.querySelector('[class*=crimeOptionSection___][class*=timeSection___]').textContent);
const bValue = parseVerbalTimestamp(b.querySelector('[class*=crimeOptionSection___][class*=timeSection___]').textContent);
return (aValue < bValue ? -1 : aValue > bValue ? 1 : 0) * sortDirection;
});
}
break;
case sortBy.Score:
childElements.sort((a, b) => {
const aValue = a.getAttribute('data-score');
const bValue = b.getAttribute('data-score');
return aValue.localeCompare(bValue, undefined, {'numeric': true}) * sortDirection;
});
break;
case sortBy.CardsSkimmed:
childElements.sort((a, b) => {
const aValue = a.querySelector('[class*=crimeOptionSection___][class*=statusSection___] [class*=statusCards___]').textContent;
const bValue = b.querySelector('[class*=crimeOptionSection___][class*=statusSection___] [class*=statusCards___]').textContent;
return aValue.localeCompare(bValue, undefined, {'numeric': true}) * sortDirection;
});
break;
default:
console.error("[TornCrimesCardSkimmingSorting] Unexpected sort values!", sortByProperty, sortDirection);
break;
}
childElements.forEach(element => {
parentElement.appendChild(element);
});
let locationsDropdown = document.querySelector('div[class*=locationSelectSection___] ul');
// Calculate the average stat score for each location and append it to dropdown option
for (let location in locationStats) {
const averageScore = (locationStats[location].totalScore / locationStats[location].totalCount).toFixed(2);
let option = locationsDropdown.querySelector(`li#option-${location.replace(' ', '-')}`);
let stats = option.querySelector('p.stats') ?? addStatsBlockToDropdownOption(option);
stats.textContent = ` ${averageScore} c/h`;
}
let totalStatsDiv = document.querySelector("div[class*=currentCrime___] div[class*=titleBar___] div.total-stats") ?? addTotalStats();
// Calculate the overall average stat score
let overallTotalScore = 0;
let overallTotalCount = 0;
for (let location in locationStats) {
overallTotalScore += locationStats[location].totalScore;
overallTotalCount += locationStats[location].totalCount;
}
const overallScore = overallTotalScore.toFixed(2);
totalStatsDiv.textContent = isMobileView ? `${overallScore} c/h - ${overallTotalCount}/20 skimmers` : `${overallScore} card/hour with ${overallTotalCount}/20 skimmers`;
}
function addTotalStats() {
const statBlock = document.createElement('div');
statBlock.className = 'total-stats';
let crimeTitle = document.querySelector("div[class*=currentCrime___] div[class*=titleBar___] div[class*=title___]");
crimeTitle.parentNode.insertBefore(statBlock, crimeTitle.nextSibling);
return statBlock;
}
function addStatsBlockToDropdownOption(element) {
const statBlock = document.createElement('p');
statBlock.className = 'stats';
element.appendChild(statBlock);
return statBlock;
}
function addHeader(element) {
const parentElement = element.querySelector('[class*=crimeOptionGroup___]:not([class*=firstGroup___])');
let header = parentElement.querySelector('[class*=crimeOption___]').cloneNode(true);
header.classList.add("silmaril-card-skimming-header");
let headerDiv = header.querySelector('[class*=sections___]');
headerDiv.style.height = "25px";
let imageDiv = header.querySelector('[class*=crimeOptionImage___]');
imageDiv.style = "display: flex;justify-content: center;align-items: center;flex-direction: row;height: 25px;";
imageDiv.innerText = "Sort by";
let nameDiv = header.querySelector('[class*=crimeOptionSection___][class*=flexGrow___]');
nameDiv.style.cursor = "pointer";
nameDiv.classList.add("silmaril-crimes-card-skimming-sorting");
nameDiv.classList.add("silmaril-crimes-card-skimming-sorting-location");
nameDiv.setAttribute("data-sort-name", "Location");
nameDiv.innerHTML = "Location ⇧⇩";
let scoreDiv = nameDiv.cloneNode(true);
scoreDiv.classList.remove("silmaril-crimes-card-skimming-sorting-location");
scoreDiv.classList.add("silmaril-crimes-card-skimming-sorting-score");
scoreDiv.setAttribute("data-sort-name", "Score");
scoreDiv.innerText = "Score ⇧⇩";
let delimiter = document.createElement("div");
delimiter.className = "sectionDelimiter___NpsSC";
if (isMobileView) {
nameDiv.parentNode.insertBefore(delimiter, nameDiv.nextSibling);
let timeDiv = nameDiv.cloneNode(true);
timeDiv.classList.remove("silmaril-crimes-card-skimming-sorting-location");
timeDiv.classList.add("silmaril-crimes-card-skimming-sorting-time-installed");
timeDiv.setAttribute("data-sort-name", "TimeInstalled");
timeDiv.innerText = "Time ⇧⇩";
delimiter.parentNode.insertBefore(timeDiv, delimiter.nextSibling);
timeDiv.parentNode.insertBefore(delimiter, timeDiv.nextSibling);
delimiter.parentNode.insertBefore(scoreDiv, delimiter.nextSibling);
} else {
let timeDiv = header.querySelector('[class*=crimeOptionSection___][class*=timeSection___]');
timeDiv.style.cursor = "pointer";
timeDiv.classList.add("silmaril-crimes-card-skimming-sorting");
timeDiv.classList.add("silmaril-crimes-card-skimming-sorting-time-installed");
timeDiv.setAttribute("data-sort-name", "TimeInstalled");
timeDiv.innerText = "Time installed ⇧⇩";
scoreDiv.style.justifyContent = 'space-around';
scoreDiv.style.width = '13px';
nameDiv.parentNode.insertBefore(delimiter, nameDiv.nextSibling);
delimiter.parentNode.insertBefore(scoreDiv, delimiter.nextSibling);
}
let cardsDiv = header.querySelector('[class*=crimeOptionSection___][class*=statusSection___]');
cardsDiv.style.cursor = "pointer";
cardsDiv.classList.add("silmaril-crimes-card-skimming-sorting");
cardsDiv.classList.add("silmaril-crimes-card-skimming-sorting-cards-skimmed");
cardsDiv.setAttribute("data-sort-name", "CardsSkimmed");
cardsDiv.innerText = isMobileView ? "Cards ⇧⇩" : "Cards skimmed ⇧⇩";
header.querySelector(`[class*=commitButtonSection___] ${isMobileView ? '' : 'button'}`).remove();
parentElement.appendChild(header);
}
function parseVerbalTimestamp(verbalTimestamp) {
const timeUnits = {
second: 1,
seconds: 1,
minute: 60,
minutes: 60,
hour: 3600,
hours: 3600,
day: 86400,
days: 86400,
week: 604800,
weeks: 604800
};
const regex = /(\d+)\s+(\w+)/g;
let totalSeconds = 0;
let match;
while ((match = regex.exec(verbalTimestamp))) {
const [, value, unit] = match;
if (timeUnits.hasOwnProperty(unit)) {
totalSeconds += parseInt(value) * timeUnits[unit];
}
}
return totalSeconds;
}
})();