OutcomeDB

Captures crime outcome, skill gain and target data for analysis

当前为 2024-09-07 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         OutcomeDB
// @namespace    de.sabbasofa.outcomedb
// @version      2.0.5
// @description  Captures crime outcome, skill gain and target data for analysis
// @author       Hemicopter [2780600], Lazerpent [2112641]
// @match        https://www.torn.com/loader.php?sid=crimes*
// @match        https://torn.com/loader.php?sid=crimes*
// @grant        GM_xmlhttpRequest
// @connect      api.lzpt.io

// ==/UserScript==

(function () {
    'use strict';

    const isTampermonkeyEnabled = typeof unsafeWindow !== 'undefined';
    const win = isTampermonkeyEnabled ? unsafeWindow : window;
    const {fetch: originalFetch} = win;
    let currentCrimesByTypeData;

    win.fetch = async (...args) => {
        let [resource, config] = args;
        return originalFetch(resource, config).then(response => detectCrimeDataRequest(resource, response));
    };
    console.log("[OutcomeDB] Watching for crime.");

    function detectCrimeDataRequest(resource, response) {
        if(!(resource.includes("sid=crimesData"))) return response;
        if (resource.includes("step=attempt")) response.clone().text().then(body => handleCrimeAttempt(body, resource));
        if (resource.includes("step=crimesList")) response.clone().text().then(body => handleCrimesList(body, resource));
        return response;
    }

    function handleCrimeAttempt(body, resource) {
        console.log("[OutcomeDB] Found crime attempt.");
        console.log("[OutcomeDB] url:", resource);

        //Most likely cloudflare turnstyle or server error
        if(body.includes("!DOCTYPE")) {
            console.error("[OutcomeDB] Unexpected HTML data, skipping...");
            return;
        }

        try {
            let data = JSON.parse(body);
            if (data.error) {
                console.log("[OutcomeDB] Failed crime attempt: " + data.error);
                console.log(JSON.stringify(data));
                return;
            }
            if (!(data.DB && data.DB.outcome)) return;
            if(data.DB.outcome.result === "error") {
                console.log("[OutcomeDB] Failed crime attempt.");
                console.log(JSON.stringify(data));
                return;
            }
            console.log("[OutcomeDB] Found outcome.");
            console.log("[OutcomeDB] Preparing bundle.");
            let bundle = {};
            bundle.outcome = data.DB.outcome;
            bundle.typeID = resource.split("typeID=")[1].split("&")[0];
            bundle.crimeID = resource.split("crimeID=")[1].split("&")[0];

            bundle.skillBefore = getStat("Skill");
            bundle.skillAfter = data.DB.currentUserStatistics[0].value;
            bundle.progressionBonus = getStat("Progression bonus");
            bundle.additionalData = getAdditionalData(data.DB.crimesByType, bundle.typeID, resource);
            if(!bundle.skillBefore || !bundle.skillAfter) {
                console.error("[OutcomeDB] Could not find skill data, skipping...");
                return;
            }

            console.log("[OutcomeDB] Ready to send bundle.", JSON.stringify(bundle));
            sendBundleToAPI(bundle);
            currentCrimesByTypeData = data.DB.crimesByType;

        } catch (e) {
            console.error("[OutcomeDB] Error parsing data:", body, e);
            alert("OutcomeDB error attempt_parse. Please check console and report this to Lazerpent [2112641] or Hemicopter [2780600].");
        }
    }

    function handleCrimesList(body, resource) {
        console.log("[OutcomeDB] Updating crimes data.");

        //Most likely cloudflare turnstyle or server error
        if(body.includes("!DOCTYPE")) {
            console.error("[OutcomeDB] Unexpected HTML data, skipping...");
            return;
        }

        try {
            let data = JSON.parse(body);
            if (data.error) {
                console.log("[OutcomeDB] Failed crimesList: " + data.error);
                console.log(JSON.stringify(data));
                return;
            }
            if (!(data.DB && data.DB.crimesByType)) return;
            currentCrimesByTypeData = data.DB.crimesByType;

        } catch (e) {
            console.error("[OutcomeDB] Error parsing data:", body, e);
            alert("OutcomeDB error list_parse. Please check console and report this to Lazerpent [2112641] or Hemicopter [2780600].");
        }
    }

    function sendBundleToAPI(bundle) {
        GM_xmlhttpRequest({
            method: "POST",
            url: "https://api.lzpt.io/outcomedb",
            headers: {"Content-Type": "application/json"},
            data: JSON.stringify(bundle),
            onload: function (response) {
                if(!response) return; // because pda doesn't know how to network i guess
                console.log("[OutcomeDB] Bundle successfully sent to API:", response.responseText);

                const json = JSON.parse(response.responseText);
                if (json.error) {
                    alert("OutcomeDB error post_invalid. Please check console and report this to Lazerpent [2112641] or Hemicopter [2780600]: " + response.responseText);
                }
            },
            onerror: function (error) {
                console.error("[OutcomeDB] Error sending bundle to API:", error);
                alert("OutcomeDB error post_error. Please check console and report this to Lazerpent [2112641] or Hemicopter [2780600]: " + error);
            }
        });
    }

    function getStat(name) {
        let allStatisticButtons = Array.from(win.document.querySelectorAll('button[class^="statistic___"]'));

        let statButton = allStatisticButtons.find(button => {
            return Array.from(button.querySelectorAll('span')).some(span => span.textContent.trim() === name);
        });

        if (statButton) {
            let valueSpan = statButton.querySelector('span[class^="value___"]');
            if (valueSpan) {
                console.log(`[OutcomeDB] Found stat (${name}): '${valueSpan.textContent}'`);
                return valueSpan.textContent;
            }
        }
        console.error(`[OutcomeDB] Could not find stat ${name}`);
    }

    function getAdditionalData(attemptData, typeID, resource) {
        try {
            if(typeID === "12") return extractScammingData(attemptData, resource);
            return null;

        } catch(error) {
            console.error("[OutcomeDB] Additional data failed, skipping:", error);
            return null;
        }
    }

    function extractScammingData(attemptData, resource) {

        //However Laz did that
        if(!currentCrimesByTypeData) return null;

        //Is it linked to a target?
        if(!(resource.includes("value1"))) return null;
        let subID = resource.split("value1=")[1].split("&")[0];

        console.log("[OutcomeDB] Extracting additional scamming data.");

        //Get target states
        let beforeTargetState = currentCrimesByTypeData.targets.find((target) => {return target.subID.includes(subID);});
        let afterTargetState = attemptData.targets.find((target) => {return target.subID.includes(subID);});

        let additionalData = {};

        //target information
        additionalData.gender = beforeTargetState.gender? beforeTargetState.gender : afterTargetState.gender;
        additionalData.target = beforeTargetState.target? beforeTargetState.target : afterTargetState.target;

        //action information
        if(!beforeTargetState.bar) additionalData.action = "read";
        else if(!afterTargetState) additionalData.action = "capitalize";
        else additionalData.action = afterTargetState.lastAction;

        const transformBar = (bar) => {
            if (!bar) return null;
            const stateMapping = {
                "neutral": "n",
                "fail": "f",
                "low": "l",
                "medium": "m",
                "high": "h",
                "sensitivity": "s",
                "temptation": "t",
                "hesitation": "w",
                "concern": "c"
            };
            return bar.map(state => stateMapping[state] || '?').join('');
        };

        if (beforeTargetState) {
            additionalData.targetBefore = {
                "multiplierUsed": beforeTargetState.multiplierUsed,
                "pip": beforeTargetState.pip,
                "turns": beforeTargetState.turns,
                "bar": transformBar(beforeTargetState.bar)
            };
        }

        if (afterTargetState) {
            additionalData.targetAfter = {
                "multiplierUsed": afterTargetState.multiplierUsed,
                "pip": afterTargetState.pip,
                "turns": afterTargetState.turns,
                "bar": transformBar(afterTargetState.bar)
            };
            additionalData.cooldown = afterTargetState.cooldown ? Math.floor(afterTargetState.cooldown - afterTargetState.timestamp) : null;
        }

        //targetAfter is missing on capitalize
        if(!additionalData.targetAfter) additionalData.targetAfter = {};

        console.log("[OutcomeDB] Additional data gathered.");
        return additionalData;
    }
})();