Torn Log Cracking

Logs detailed info about all cracking attempts.

// ==UserScript==
// @name         Torn Log Cracking
// @namespace    https://github.com/SOLiNARY
// @version      0.1.13
// @description  Logs detailed info about all cracking attempts.
// @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_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_setClipboard
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    const viewPortWidthPx = window.innerWidth;
    const isMobileView = viewPortWidthPx <= 784;
    const isTampermonkeyEnabled = typeof unsafeWindow !== 'undefined';
    const levelsImagePxGap = 35;
    const levelsArray = [[1], generateNumberRange(2, 24), generateNumberRange(25, 49), generateNumberRange(50, 74), generateNumberRange(75, 99)];
    const actions = ['Brute force', 'Crack'];
    let attached = false;
    let userId = 'N/A';

    if (!isTampermonkeyEnabled) {
        console.error('[TornLogCracking] Please, install Tampermonkey first!');
        alert('[TornLogCracking] Please, install Tampermonkey first!');
        return;
    }

    const styles = `
    div.crimeSlider___uha7d > div.previousCrime___gxp9r, div.currentCrime___KNKYQ, div.nextCrime___uKnJr {
        transition-duration: 0ms !important;
    }

    div.silmarilPopOutText {
      position: absolute;
      color: #37b24d;
      font-size: 16px;
      display: none;
      user-select: none;
      pointer-events: none;
      text-shadow: 0.5px 0.5px 0.5px black, 0 0 1em black, 0 0 0.2em black;
    }`;
    GM_addStyle(styles);

    try {
        userId = document.cookie.split('; ').filter( x => x.startsWith('uid=') )[0].split('=')[1];
    } catch (e) {
        userId = 'N/A';
    }

    addExportButton(document.querySelector('div.crimes-app div[class*=appHeader___]'));

    const targetNode = document.querySelector("div.crimes-app");
    const observerConfig = { childList: true, subtree: true };
    const observer = new MutationObserver(async (mutationsList, observer) => {
        const crimesDiv = document.querySelectorAll("div.crimes-app");
        const currentCrimeDiv = document.querySelectorAll("div[class*=currentCrime___]");
        for (const mutation of mutationsList) {
            let mutationTarget = mutation.target;
            if (!attached && mutation.type === 'childList' && mutationTarget.className == 'crime-root cracking-root') {
                if (document.querySelector('div[class*=currentCrime___] div[class*=title___]') == null) {
                    continue;
                }
                attached = true;
                crimesDiv.forEach((div) => {
                    div.addEventListener("click", function (event) {
                        if (event.target.matches("div.topSection___U7sVi div.linksContainer___LiOTN")) {
                            handleSwitchPage();
                        }
                    });
                });
                currentCrimeDiv.forEach((div) => {
                    div.addEventListener("click", function (event) {
                        if (event.target.matches("div.topSection___HchKK div.crimeBanner___LiKtj div.crimeSliderArrowButtons___N_y4N button.arrowButton___gYTVW")) {
                            handleSwitchPage();
                        }
                    });
                });
                $(mutationTarget).on("click", "button.commitButton___NYsg8:not(.silmaril-log-cracking)", async function(event){
                    try {
                        console.log('muta click', mutationTarget, event);
                        const now = document.querySelector('span.server-date-time').textContent.split(' ');
                        const date = now[3];
                        const time = now[1];
                        const skillLevelBefore = getCurrentSkillLevelValueV2();

                        const button = findNearestButtonParent(event.target);
                        const action = button.ariaLabel;
                        const row = button.parentNode.parentNode;
                        let passwordLength = '';
                        let passwordBefore = '';

                        try {
                            switch (action) {
                                case 'Brute force, 7 nerve':
                                case 'Crack, 5 nerve': {
                                    const passwordChars = isMobileView ? row.querySelectorAll('div.tabletPassword___hnzbl > div:not(.charSlotDummy___s11h5)') : row.querySelectorAll('div.desktopPasswordSection___tucU6 > div:not(.charSlotDummy___s11h5)');
                                    passwordLength = passwordChars.length;
                                    passwordBefore = getCurrentPassword(passwordChars);
                                    break;
                                }
                                default:
                                    break;
                            }
                        } catch (e) {
                            console.error('[TornLogCracking] Error while parsing action', action, e);
                        }

                        while (row.nextElementSibling.querySelector('div.outcomeReward___E34U7:not(.silmaril-log-cracking-parsed)') == null) {
                            await sleep(20);
                        }

                        const outcomeRewardDiv = row.nextElementSibling.querySelector('div.outcomeReward___E34U7');
                        const result = outcomeRewardDiv.querySelector('p.title___IrNe6').textContent;
                        const gained = [];
                        if (outcomeRewardDiv.querySelector('div.rewards___oRCyz span.reward___dCLvW') != null) {
                            const money = outcomeRewardDiv.querySelector('div.rewards___oRCyz span.reward___dCLvW').textContent;
                            gained.push(money);
                        }
                        try {
                            gained.push(outcomeRewardDiv.querySelector('div.rewards___oRCyz div.reward___Tvvmx').textContent);
                        } catch (e) {
                            console.log('[TornLogCracking] No reward string parsed');
                        }
                        try {
                            gained.push(outcomeRewardDiv.querySelector('div.rewards___oRCyz div.reward___OmEOo').textContent);
                        } catch (e) {
                            console.log('[TornLogCracking] No crit fail injury string parsed');
                        }
                        try {
                            gained.push(outcomeRewardDiv.querySelector('div.rewards___oRCyz div.reward___snxxf').textContent);
                        } catch (e) {
                            console.log('[TornLogCracking] No crit fail jail string parsed');
                        }

                        outcomeRewardDiv
                            .querySelectorAll('div.rewards___oRCyz div.reward___P7K87 img')
                            .forEach(item => {
                            try {
                                gained.push(item.alt);
                            } catch (e) {
                                console.error('[TornLogCracking] Error while parsing non-ammo rewards:', item, e);
                            }
                        });
                        outcomeRewardDiv
                            .querySelectorAll('div.rewards___oRCyz div.reward___oQH1h')
                            .forEach(item => {
                            try {
                                let ammoQuantity = item.querySelector('div.itemCell___aZaUE.countCell___XzhYQ span.count___hBmtm').textContent;
                                let ammoCodeX = (Math.abs(parseInt(item.querySelector('div.ammoImage___tebhe').style.backgroundPositionX))/70).toFixed(0);
                                let ammoCodeY = (Math.abs(parseInt(item.querySelector('div.ammoImage___tebhe').style.backgroundPositionY))/70).toFixed(0);
                                gained.push(`${ammoQuantity} ammo of type ${ammoCodeX}-${ammoCodeY}`);
                            } catch (e) {
                                console.error('[TornLogCracking] Error while parsing ammo rewards:', item, e);
                            }
                        });
                        outcomeRewardDiv.classList.add('silmaril-log-cracking-parsed');
                        const skillLevelAfter = getCurrentSkillLevelValueV2();

                        let passwordAfter = '';
                        switch (action) {
                            case 'Brute force, 7 nerve':
                            case 'Crack, 5 nerve': {
                                const passwordChars = isMobileView ? row.querySelectorAll('div.tabletPassword___hnzbl > div:not(.charSlotDummy___s11h5)') : row.querySelectorAll('div.desktopPasswordSection___tucU6 > div:not(.charSlotDummy___s11h5)');
                                passwordAfter = getCurrentPassword(passwordChars);
                                break;
                            }
                            default:
                                break;
                        }

                        const progressionBonus = document.querySelector('div[class*=statsColumn___] > button[class*=statistic___]:nth-child(3) > span[class*=value___]').textContent;
                        const version = GM_info.script.version;
                        // playerId, date, time, action, skillBefore, skillAfter, targetType, targetService, rigStatus, rigPower, rigMips, rigBruteForceStrength, rigOverheatedComponents, rigHottestComponent, result, gained, progressionBonus, version
                        const targetDiv = row.querySelector('div[class*=targetSection___] div[class*=typeAndServiceWrapper___] div[class*=typeAndService___]');
                        const targetType = targetDiv.querySelector('span[class*=type___]').innerText;
                        const targetService = targetDiv.querySelector('span[class*=service___]').innerText;

                        const rigDiv = document.querySelector('div[class*=rig___]');
                        const rigStatus = rigDiv.querySelector('div[class*=heading___] span[class*=status___]').innerText;
                        const rigPower = rigDiv.querySelector('div[class*=power___] span[class*=value___]').innerText;
                        const rigMips = rigDiv.querySelector('div[class*=statistic___][class*=mips___] span[class*=value___]').innerText;
                        const rigBruteForceStrength = rigDiv.querySelector('div[class*=statistic___][class*=strength___] span[class*=value___]').innerText;
                        const rigOverheatedComponents = rigDiv.querySelector('div[class*=statistic___][class*=overheated___] span[class*=value___]').innerText;
                        const rigHottestComponent = rigDiv.querySelector('div[class*=statistic___][class*=hottest___] span[class*=value___]').innerText;
                        let rigClockSpeed = '';
                        try {
                            const computedStyle = getComputedStyle(rigDiv.querySelector('div[class*=readOnlyKnob___]'));
                            rigClockSpeed = computedStyle.getPropertyValue('--position-on-bar');
                        } catch (e) {
                            rigClockSpeed = 'N/A';
                        }

                        const crackingAttempt = new CrackingAttempt(userId, date, time, action, skillLevelBefore, skillLevelAfter, targetType, targetService, passwordLength, passwordBefore, passwordAfter, rigStatus, rigPower, rigMips, rigBruteForceStrength, rigOverheatedComponents, rigHottestComponent, rigClockSpeed, result, gained, progressionBonus, version);
                        const dateSplit = date.split('/');
                        const timeStampFormatted = `${dateSplit[2]}${dateSplit[1]}${dateSplit[0]}${time.replaceAll(':','')}`;
                        const newGuid = generateGuid();
                        GM_setValue(`silmaril-torn-log-cracking-attempt-${timeStampFormatted}-${newGuid}`, crackingAttempt);
                        const logsTotal = updateLogsCount();
                        showPopOutText(`Logged +1! (${logsTotal})`, event.clientX, event.clientY);
                    } catch (e) {
                        console.error('[TornLogCracking] Error while constructing log', e);
                        showPopOutText('Not Logged!', event.clientX, event.clientY, false);
                    }
                });

                observer.disconnect();
            }
        }
    });
    setUpObserver();

    function getCurrentPassword(passwordChars) {
        let password = '';
        console.log('passwordChars', passwordChars);
        passwordChars.forEach((item, index) => {
            try {
                console.log('char item', item);
                const passChar = item.querySelector('span.discoveredChar___mmchE');
                if (passChar != null)
                {
                    password += passChar.innerText;
                } else {
                    password += item.querySelector('div.encryption1___w7tm_') != null ? '#' : '?';
                }

            }
            catch (e) {
                password += '!';
            }
        });
        console.log('password returned', password);
        return password;
    }

    function handleSwitchPage() {
        attached = false;
        setUpObserver();
    }

    function getRigInfo() {
    }

    function addExportButton(root) {
        const outerExportDiv = document.createElement('div');
        outerExportDiv.className = 'title___MqBua';
        const exportButton = document.createElement('button');
        exportButton.className = 'torn-btn commitButton___uX7k_ silmaril-log-cracking silmaril-log-cracking-export';
        exportButton.ariaLabel = 'Copy Cracking logs';
        const exportButtonLabel = document.createElement('span');
        exportButtonLabel.className = 'title___KAQ9m';
        exportButtonLabel.textContent = isMobileView ? 'Copy logs' : 'Copy Cracking logs';
        const exportButtonDivider = document.createElement('span');
        exportButtonDivider.className = 'divider___RGx7a';
        const exportButtonLogCount = document.createElement('span');
        exportButtonLogCount.className = 'nerveCost___OoGfz';
        exportButtonLogCount.textContent = getAllLogsCount();
        exportButton.append(exportButtonLabel, exportButtonDivider, exportButtonLogCount);
        exportButton.addEventListener('click', exportLogs);

        const outerClearDiv = document.createElement('div');
        outerClearDiv.className = 'title___MqBua';
        const clearButton = document.createElement('button');
        clearButton.className = 'torn-btn commitButton___uX7k_ silmaril-log-cracking silmaril-log-cracking-clear';
        clearButton.ariaLabel = 'Clear Cracking logs';
        const clearButtonLabel = document.createElement('span');
        clearButtonLabel.textContent = isMobileView ? 'Clear logs' : 'Clear Cracking logs';
        clearButton.appendChild(clearButtonLabel);
        clearButton.addEventListener('click', clearLogs);

        outerExportDiv.append(exportButton);
        outerClearDiv.append(clearButton);

        root.append(outerExportDiv, outerClearDiv);

        const backButton = document.querySelector('div[class*=appHeader___] > a[class*=link___]');
        backButton.addEventListener('click', function(){
            attached = false;
            setUpObserver();
        });
    }

    function setUpObserver() {
        observer.observe(targetNode, observerConfig);
    }

    function updateLogsCount(reset = false) {
        const countSpan = document.querySelector('button.silmaril-log-cracking-export > span.nerveCost___OoGfz');
        const count = reset ? 0 : parseInt(countSpan.textContent) + 1;
        countSpan.textContent = count;
        return count;
    }

    function exportLogs() {
        let logs = getAllLogs();
        let logsRaw = JSON.stringify(logs);
        GM_setClipboard(logsRaw, 'json');
        const exportText = document.querySelector('button.silmaril-log-cracking-export > span.title___KAQ9m');
        exportText.textContent = isMobileView ? 'Copied!' : 'Copied to clipboard!';
        setTimeout(function(){exportText.textContent = isMobileView ? 'Copy logs' : 'Copy Cracking logs';}, 2500);
    }

    function clearLogs() {
        if (!confirm('Are you sure you want to clear the logs?\r\nMake sure to send them to Emforus first.')) {
            return;
        }
        deleteAllLogs();
        updateLogsCount(true);
        const clearText = document.querySelector('button.silmaril-log-cracking-clear > span');
        clearText.textContent = isMobileView ? 'Cleared!' : 'Cleared all logs!';
        setTimeout(function(){clearText.textContent = isMobileView ? 'Clear logs' : 'Clear Cracking logs';}, 2500);
    }

    function getAllLogs() {
        const prefix = 'silmaril-torn-log-cracking-attempt-';
        const filteredRecords = {};
        const allKeys = GM_listValues();
        allKeys.forEach((key) => {
            if (key.startsWith(prefix)) {
                filteredRecords[key] = GM_getValue(key);
            }
        });
        return filteredRecords;
    }

    function getAllLogsCount() {
        const allLogsCount = Object.keys(GM_listValues()).length;
        return allLogsCount;
    }

    function deleteAllLogs() {
        const allKeys = GM_listValues();
        allKeys.forEach((key) => {
            GM_deleteValue(key);
        });
    }

    function getCurrentSkillLevelValueV2() {
        let skillLevel = 'N/A';
        try {
            skillLevel = document.querySelector('[class*=value___][class*=copyTrigger___]').textContent;
        } catch (e) {
            console.error('[TornLogCracking] Error while parsing skill level using v2', e);
            return skillLevel;
        }
        console.log('skillLevel parsed', skillLevel);
        return skillLevel;

    }

    function getCurrentSkillLevelValue() {
        let currentSkillLevel = -1;
        try {
            if (document.querySelector('div.maxLevel___jTooz') == null) {
                const computedStyle = getComputedStyle(document.querySelector('div.topSection___HchKK div.currentLevel___vCRVm div.number___L1VcJ'));
                const levelFirstIndex = Math.round(Math.abs(parseInt(computedStyle.getPropertyValue('background-position-x'), 10)) / levelsImagePxGap);
                const levelSecondIndex = Math.round(Math.abs(parseInt(computedStyle.getPropertyValue('background-position-y'), 10)) / levelsImagePxGap);
                const skillLevelBefore = levelsArray[levelFirstIndex][levelSecondIndex];
                const skillProgressBar = document.querySelector('div.progressFill___ksrq5');
                const skillPercentageBefore = parseInt(skillProgressBar.style.width, 10) + (parseInt(skillProgressBar.style.left, 10) ?? -100);
                currentSkillLevel = skillLevelBefore + skillPercentageBefore / 100;
            } else {
                currentSkillLevel = 100;
            }
        } catch (e) {
            console.error('[TornLogCracking] Error while parsing skill level', e);
        }
        return parseFloat(currentSkillLevel.toFixed(2));
    }

    function findNearestButtonParent(element) {
        let currentElement = element;
        while (currentElement && currentElement.tagName !== 'BUTTON') {
            currentElement = currentElement.parentNode;
        }
        return currentElement;
    }

    function getTextValueWithoutChildren(element) {
        let text = '';
        for (let node of element.childNodes) {
            if (node.nodeType === Node.TEXT_NODE) {
                text += node.textContent;
            }
        }
        return text.trim();
    }

    function generateGuid() {
        const crypto = window.crypto || window.msCrypto;

        if (!crypto) {
            return Math.floor(Math.random() * (999999 - 100000) + 100000).toString();
        }

        const data = new Uint16Array(8);
        crypto.getRandomValues(data);

        data[3] = (data[3] & 0x0fff) | 0x4000; // Version 4
        data[4] = (data[4] & 0x3fff) | 0x8000; // Variant 10

        const guid = Array.from(data)
        .map((byte) => byte.toString(16).padStart(2, '0'))
        .join('');

        return (
            guid.substr(0, 8) + '-' +
            guid.substr(8, 4) + '-' +
            guid.substr(12, 4) + '-' +
            guid.substr(16, 4) + '-' +
            guid.substr(20, 12)
        );
    }

    function generateNumberRange(start, end) {
        return Array.from({ length: end - start + 1 }, (_, index) => start + index);
    }

    function showPopOutText(text, mouseX, mouseY, isSuccess = true, delay = 2000) {
        // Create pop-out text element
        let popOutText = document.createElement('div');
        popOutText.className = 'silmarilPopOutText';
        popOutText.style.color = isSuccess ? '#37b24d' : '#f03e3e';
        popOutText.innerText = text;

        // Append element to the body
        document.body.appendChild(popOutText);

        // Adjust for scroll position
        let scrollX = window.scrollX || window.pageXOffset;
        let scrollY = window.scrollY || window.pageYOffset;

        // Set random position within a specific radius
        let minAngle = 0.4; // Adjust this angle as needed
        let maxAngle = 0.6;
        let minRadius = 50; // Adjust this radius as needed
        let maxRadius = 250;
        let angle = (Math.random() * (maxAngle - minAngle) + minAngle) * Math.PI * 2;
        let radius = (Math.random() * (maxRadius - minRadius) + minRadius);
        let randomX = mouseX + Math.cos(angle) * radius + scrollX;
        let randomY = mouseY + Math.sin(angle) * radius + scrollY;

        // Set pop-out text position
        popOutText.style.left = randomX + 'px';
        popOutText.style.top = randomY + 'px';

        // Show pop-out text
        popOutText.style.display = 'block';

        // Fade away and disappear after a few seconds
        setTimeout(function() {
            popOutText.style.opacity = 0;
            setTimeout(function() {
                document.body.removeChild(popOutText);
            }, 500); // 500ms delay for removal after fade
        }, delay); // 2000ms (2 seconds) delay for fade
    }

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

    class CrackingAttempt {
        constructor(playerId, date, time, action, skillBefore, skillAfter, targetType, targetService, passwordLength, passwordBefore, passwordAfter, rigStatus, rigPower, rigMips, rigBruteForceStrength, rigOverheatedComponents, rigHottestComponent, rigClockSpeed, result, gained, progressionBonus, version) {
            this.playerId = playerId;
            this.date = date;
            this.time = time;
            this.action = action;
            this.skillBefore = skillBefore;
            this.skillAfter = skillAfter;
            this.targetType = targetType;
            this.targetService = targetService;
            this.passwordLength = passwordLength;
            this.passwordBefore = passwordBefore;
            this.passwordAfter = passwordAfter;
            this.rigStatus = rigStatus;
            this.rigPower = rigPower;
            this.rigMips = rigMips;
            this.rigBruteForceStrength = rigBruteForceStrength;
            this.rigOverheatedComponents = rigOverheatedComponents;
            this.rigHottestComponent = rigHottestComponent;
            this.rigClockSpeed = rigClockSpeed;
            this.result = result;
            this.gained = gained;
            this.progressionBonus = progressionBonus;
            this.version = version;
        }
    }
})();