Torn Crimes Card Skimming Extended

Sorts all installed card skimmers by location, time installed, score or cards skimmed. Adds card/hour stat. Remembers your choice.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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;
    }
})();