Torn Crimes Cracking Difficulty

Shows difficulty of cracking targets in cycles & nerve to be spent.

// ==UserScript==
// @name         Torn Crimes Cracking Difficulty
// @namespace    https://github.com/SOLiNARY
// @version      0.1.3
// @description  Shows difficulty of cracking targets in cycles & nerve to be spent.
// @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        GM_addStyle
// @grant        unsafeWindow
// ==/UserScript==

(async function() {
    'use strict';

    const crackingHash = '#/cracking';
    const difficultyInfoTemplate = '{cycles} cycles, {attempts} attempts, {nerve} nerve needed';
    const isTampermonkeyEnabled = typeof unsafeWindow !== 'undefined';
    const isMobileView = window.innerWidth <= 784;
    const difficultyColorMap = {
        "OneAttempt": "#37b24d",
        "ThreeOrLess": "#74b816",
        "FiveOrLess": "#f59f00",
        "SevenOrLess": "#f76707",
        "TenOrLess": "#f03e3e",
        "MoreThanTen": "#7048e8",
    };
    let isOnCrackingPage = false;
    let bruteForceStrength = 0;

    const styles = `
@media only screen and (max-width: 784px) {
    span.silmaril-crimes-cracking-difficulty {
        font-size: xx-small;
    }

    div.crime-root.cracking-root div[class^=crimeOptionWrapper___] div[class^=sections___] {
        height: 54px !important;
    }
}
`;

    if (isTampermonkeyEnabled){
        GM_addStyle(styles);
    } else {
        let style = document.createElement("style");
        style.type = "text/css";
        style.innerHTML = styles;
        while (document.head == null){
            await sleep(50);
        }
        document.head.appendChild(style);
    }

    checkURLChange();

    setInterval(checkURLChange, 750);

    const targetNode = document.querySelector("div.crimes-app");
    const observerConfig = { childList: true, subtree: true };
    const observer = new MutationObserver(async (mutationsList, observer) => {
        for (const mutation of mutationsList) {
            if (!isOnCrackingPage) {
                break;
            }

            let mutationTarget = mutation.target;
            if (mutation.type === 'childList' && mutationTarget.className.indexOf('outcomeWrapper___') >= 0) {
                let outcomeDiv = mutationTarget.querySelector('div[class*=outcome___]');
                if (outcomeDiv == null || outcomeDiv.hasAttribute('data-cracking-difficulty-value-set')) {
                    continue;
                }
                outcomeDiv.setAttribute('data-cracking-difficulty-value-set', '');

                const crimeOption = mutationTarget.parentNode.querySelector('div.crime-option');
                if (crimeOption.classList.contains('crime-option')){
                    setTimeout(function() {calculateDifficulty(crimeOption)}, 500);
                }
                break;
            }
        }
    });
    observer.observe(targetNode, observerConfig);

    async function addDifficulty() {
        while (document.querySelector('div.crime-root.cracking-root div[class^=currentCrime___]') == null) {
            if (!isOnCrackingPage) {
                break;
            }
            await sleep(50);
        }
        if (!isOnCrackingPage) {
            return;
        }

        const targets = document.querySelectorAll('div[class^=currentCrime___] div[class^=virtualList___] div[class^=crimeOptionWrapper___] div.crime-option');
        while (document.querySelector('div[class^=rig___]') == null){
            await sleep(50);
        }
        bruteForceStrength = parseFloat(document.querySelector('div[class^=rig___] div[class^=statistics___] div[class*=strength___] span[class^=value___]').innerText);

        targets.forEach(target => calculateDifficulty(target));
    }

    function calculateDifficulty(target) {
        const targetDescription = target.querySelector('div[class*=targetSection___]');

        let cyclesNeeded = 0;
        const characters = target.querySelectorAll('div[class^=sections___] div[class^=charSlot___]');
        characters.forEach(character => {
            cyclesNeeded += parseDifficulty(character) + 1;
        });
        const attemptsNeeded = Math.ceil(cyclesNeeded / bruteForceStrength);
        const nerveNeeded = Math.round(attemptsNeeded * 7 + 5);
        updateDifficultyInfo(targetDescription, cyclesNeeded, attemptsNeeded, nerveNeeded);
    }

    function updateDifficultyInfo(targetDescription, cyclesNeeded, attemptsNeeded, nerveNeeded) {
        const difficultyInfoSpan = targetDescription.querySelector('span.silmaril-crimes-cracking-difficulty') ?? document.createElement('span');
        difficultyInfoSpan.className = 'silmaril-crimes-cracking-difficulty';
        switch (attemptsNeeded) {
            case 0:
            case 1:
                difficultyInfoSpan.style.color = difficultyColorMap.OneAttempt;
                break;
            case 2:
            case 3:
                difficultyInfoSpan.style.color = difficultyColorMap.ThreeOrLess;
                break;
            case 4:
            case 5:
                difficultyInfoSpan.style.color = difficultyColorMap.FiveOrLess;
                break;
            case 6:
            case 7:
                difficultyInfoSpan.style.color = difficultyColorMap.SevenOrLess;
                break;
            case 8:
            case 9:
            case 10:
                difficultyInfoSpan.style.color = difficultyColorMap.TenOrLess;
                break;
            default:
                difficultyInfoSpan.style.color = difficultyColorMap.MoreThanTen;
                break;
        }
        difficultyInfoSpan.innerText = difficultyInfoTemplate.replace('{cycles}', cyclesNeeded).replace('{attempts}', attemptsNeeded).replace('{nerve}', nerveNeeded);
        if (isMobileView) {
            targetDescription.querySelector('div[class^=typeAndServiceWrapper___]').append(difficultyInfoSpan);
        } else {
            targetDescription.querySelector('div[class^=typeAndService___]').append(difficultyInfoSpan);
        }

    }

    function checkURLChange() {
        const currentURL = window.location.href;
        if (currentURL !== checkURLChange.previousURL) {
            if (window.location.href.includes(crackingHash)) {
                isOnCrackingPage = true;
                addDifficulty();
            } else {
                isOnCrackingPage = false;
            }
        }
        checkURLChange.previousURL = currentURL;
    }

    function parseDifficulty(element) {
        const label = element.getAttribute('aria-label');
        if (label == null) {
            return 0;
        }
        if (element.querySelector('span[class^=discoveredChar___]') != null) {
            return -1;
        }
        const matchEncrypted = label.match(/, (\d+) encryption/);
        if (matchEncrypted && matchEncrypted[1]) {
            return parseInt(matchEncrypted[1]);
        }
        return null;
    }

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
})();