Torn Crime Chain Bonus Tracker

Track crime chain bonus from Torn logs & webpages realtime

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn Crime Chain Bonus Tracker
// @namespace    http://tampermonkey.net/
// @author       kaeru [1769499]
// @version      1.0
// @description  Track crime chain bonus from Torn logs & webpages realtime
// @match        https://www.torn.com/loader.php?sid=crimes*
// @grant        none
// @license MIT
// ==/UserScript==

/* jshint esversion: 8 */

(function () {
    'use strict';

    const API_KEY = ''; // Replace with your Torn API key (Full Access)
    const STORAGE_TS_KEY = 'torn_chain_last_ts';
    const STORAGE_BONUS_KEY = 'torn_chain_last_bonus';
    const STORAGE_RUN_KEY = 'torn_chain_last_run';
    const LOG_LIMIT = 1000;
    const MIN_INTERVAL = 30 * 1000; // 30 seconds

    let lastTimestamp = parseInt(localStorage.getItem(STORAGE_TS_KEY)) || 0;
    let lastBonus = parseFloat(localStorage.getItem(STORAGE_BONUS_KEY)) || 0;

    let lastDomSuccesses = null;
    let lastDomFails = null;
    let lastDomCriticalFails = null;

    let errorMessage = null;

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

    async function fetchLogsBackward(toTimestamp) {
        let currentTo = toTimestamp;
        const allLogs = [];

        if (!API_KEY) {
            errorMessage = "No API Key";
            return [];
        }

        while (true) {
            const url = `https://api.torn.com/user/?selections=log&cat=136&to=${currentTo}&key=${API_KEY}`;
            const res = await fetch(url);
            const data = await res.json();

            if (data.error) {
                console.error(`[CrimeChainBonus] API Error: ${data.error.code} - ${data.error.error}`);
                errorMessage = data.error.error;
                break;
            } else {
                errorMessage = null;
            }

            if (!data || !data.log) break;

            const logs = Object.entries(data.log)
                .map(([ts, entry]) => ({ timestamp: parseInt(ts), ...entry }))
                .sort((a, b) => b.timestamp - a.timestamp);

            if (logs.length === 0) break;

            if (allLogs.length >= LOG_LIMIT) break;

            for (const log of logs) {
                if (log.timestamp <= lastTimestamp) {
                    return allLogs; // Stop fetching if we've reached known timestamp
                }
                if (log.title.toLowerCase().startsWith('crime critical fail')) {
                    return allLogs;
                }
                allLogs.push(log);
                console.log(log);
            }

            currentTo = logs[logs.length - 1].timestamp;

            // Move backward using the oldest timestamp returned
            // Wait 1 second before next request
            await sleep(1000);
        }

        return allLogs;
    }

    function applyLogsIncrementally(baseBonus, logs) {
        let bonus = baseBonus;

        // Process logs from oldest to newest
        for (const log of logs.reverse()) {
            const title = (log.title || '').toLowerCase();

            if (title.startsWith('crime success')) {
                bonus = bonus + 1;
            } else if (title.startsWith('crime fail')) {
                bonus = bonus / 2;
            } else if (title.startsWith('crime critical fail')) {
                bonus = 0;
            }
        }

        return bonus;
    }

    function displayBonus(bonus) {
        const container = document.querySelector('.crimes-app [class*="resultCounts"]');
        if (!container) return;

        let el = container.querySelector('#chain-bonus-display');
        if (!el) {
            el = document.createElement('div');
            el.id = 'chain-bonus-display';
            el.style.fontSize = '12px';

            const label = document.createElement('span');
            label.textContent = 'Crime Chain Bonus ';

            const value = document.createElement('span');
            value.id = 'chain-bonus-value';
            value.style.color = 'rgb(103, 140, 0)';
            if (errorMessage) {
                value.textContent = errorMessage;
            } else {
                value.textContent = bonus.toFixed(2);
            }

            el.appendChild(label);
            el.appendChild(value);
            container.prepend(el); // Insert at the top
        } else {
            const value = el.querySelector('#chain-bonus-value');
            if (errorMessage) {
                value.textContent = errorMessage;
            } else if (value) {
                value.textContent = bonus.toFixed(2);
            } else {
                el.textContent = `Crime Chain Bonus `;
                const span = document.createElement('span');
                span.id = 'chain-bonus-value';
                span.style.color = 'rgb(103, 140, 0)';
                span.textContent = bonus.toFixed(2);
                el.appendChild(span);
            }
        }
    }

    async function updateBonus() {
        const now = Date.now();
        const toTimestamp = Math.floor(now / 1000) + 10;
        const lastRun = parseInt(localStorage.getItem(STORAGE_RUN_KEY)) || 0;

        if (now - lastRun < MIN_INTERVAL) {
            console.log('[CrimeChainBonus] Skipping update (too soon)');
            displayBonus(lastBonus);
            return;
        }

        const newLogs = await fetchLogsBackward(toTimestamp);
        if (newLogs.length === 0) {
            displayBonus(lastBonus);
            return;
        }

        const newTimestamp = newLogs[0].timestamp;
        const newBonus = applyLogsIncrementally(lastBonus, newLogs);

        // Store latest timestamp and bonus
        localStorage.setItem(STORAGE_TS_KEY, newTimestamp);
        localStorage.setItem(STORAGE_BONUS_KEY, newBonus);
        localStorage.setItem(STORAGE_RUN_KEY, now);

        // Update display
        displayBonus(newBonus);

        // Update current state
        lastTimestamp = newTimestamp;
        lastBonus = newBonus;
    }

    function parseCount(str) {
        return parseInt(str.replace(/,/g, ''), 10) || 0;
    }

    function readCrimeCounts() {
        const root = document.querySelector('.crimes-app');
        if (!root) return { success: null, fail: null, critical: null };

        const successEl = root.querySelector('[class*="resultCounts"] [class*="successes"] span');
        const failEl = root.querySelector('[class*="resultCounts"] [class*="fails"] span');
        const criticalEl = root.querySelector('[class*="resultCounts"] [class*="criticalFails"] span');

        const parseOrNull = el => el ? parseCount(el.textContent) : null;

        return {
            successes: parseOrNull(successEl),
            fails: parseOrNull(failEl),
            criticalFails: parseOrNull(criticalEl),
        };
    }

    function handleDomChangeByCounts() {
        const { successes, fails, criticalFails } = readCrimeCounts();

        console.log(`Crime successes: ${successes}`);
        console.log(`Crime fails: ${fails}`);
        console.log(`Crime critialFails: ${criticalFails}`);

        const wasValid =
              lastDomSuccesses !== null &&
              lastDomFails !== null &&
              lastDomCriticalFails !== null;

        const isValid = successes !== null && fails !== null && criticalFails !== null;

        if (!wasValid || !isValid) {
            // Just track values, don't calculate bonus
            lastDomSuccesses = successes;
            lastDomFails = fails;
            lastDomCriticalFails = criticalFails;

            displayBonus(lastBonus);

            return;
        }

        const dSuccess = successes - lastDomSuccesses;
        const dFail = fails - lastDomFails;
        const dCritical = criticalFails - lastDomCriticalFails;

        if (dSuccess <= 0 && dFail <= 0 && dCritical <= 0) return;

        let bonus = lastBonus;

        if (dCritical > 0) {
            bonus = 0;
        } else if (dFail > 0) {
            bonus = Math.floor(bonus / Math.pow(2, dFail));
        }

        if (dSuccess > 0) {
            bonus = bonus + dSuccess;
        }

        lastBonus = bonus;
        displayBonus(bonus);

        lastDomSuccesses = successes;
        lastDomFails = fails;
        lastDomCriticalFails = criticalFails;
    }

    // Initial call
    updateBonus();

    // Observe DOM changes and trigger bonus update
    const observer = new MutationObserver(() => {
        handleDomChangeByCounts();
    });

    observer.observe(document.body, { childList: true, subtree: true });
})();