OC Success Chance 2.0

Optimized OC success chance calculator

目前為 2025-03-06 提交的版本,檢視 最新版本

// ==UserScript==
// @name         OC Success Chance 2.0
// @namespace    http://tampermonkey.net/
// @version      2.0.7
// @description  Optimized OC success chance calculator
// @author       Allenone [2033011]
// @match        https://www.torn.com/factions.php?step=your*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @connect      tornprobability.com
// @grant        GM.xmlHttpRequest
// @grant        GM_info
// ==/UserScript==

(function () {
    'use strict';
    const ocs = new Map();
    let observer;
    let processing = false;
    const debounceDelay = 300;

    // Centralized error handling
    const logError = (message, error) => {
        console.error(`[OC Success] ${message}`, error?.message || error);
    };

    // Debounce function for performance
    function debounce(func, wait) {
        let timeout;
        return (...args) => {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }

    // Generic API caller
    async function callOCAPI(endpoint, data) {
        return new Promise((resolve, reject) => {
            GM.xmlHttpRequest({
                method: 'POST',
                url: `https://tornprobability.com:3000/${endpoint}`,
                headers: {'Content-Type': 'application/json'},
                data: JSON.stringify(data),
                onload: (response) => {
                    try {
                        resolve(JSON.parse(response.responseText));
                    } catch (err) {
                        logError('API parse error', err);
                        reject(err);
                    }
                },
                onerror: (err) => {
                    logError('API request failed', err);
                    reject(err);
                }
            });
        });
    }

    // Process individual OC element
    async function processOCElement(element) {
        const ocName = element.querySelector('.panelTitle___aoGuV')?.textContent;
        if (!ocName || !['Blast From The Past', 'Break The Bank'].includes(ocName)) return;

        const slots = element.querySelectorAll('.wrapper___Lpz_D');
        const successChances = {};

        for (const slot of slots) {
            try {
                const fiberKey = Object.keys(slot).find(k => k.startsWith('__reactFiber$'));
                const key = slot[fiberKey]?.return?.key;
                const chanceText = slot.querySelector('.successChance___ddHsR')?.textContent;

                if (key && chanceText) {
                    successChances[key] = parseFloat(chanceText.replace('%', ''));
                }
            } catch (error) {
                logError('Slot processing error', error);
            }
        }

        if (Object.keys(successChances).length === 0) return;

        try {
            const endpoint = ocName.replace(/ /g, '');
            const { successChance } = await callOCAPI(endpoint, successChances);

            if (successChance) {
                ocs.set(ocName, successChance);
                injectSuccessChance(element, successChance);
            }
        } catch (error) {
            logError('OC processing failed', error);
        }
    }

    // Inject success display
    function injectSuccessChance(element, chance) {
        const existing = element.querySelector('.oc-success-chance');
        if (existing) {
            existing.textContent = `Success: ${(chance * 100).toFixed(2)}%`;
            return;
        }

        const display = document.createElement('div');
        display.className = 'oc-success-chance';
        display.textContent = `Success: ${(chance * 100).toFixed(2)}%`;
        element.querySelector('.panelTitle___aoGuV')?.after(display);
    }

    // Main processing function
    const processOCs = debounce(() => {
        if (processing) return;
        processing = true;

        requestIdleCallback(() => {
            try {
                document.querySelectorAll('.wrapper___U2Ap7:not(.oc-processed)').forEach(async element => {
                    element.classList.add('oc-processed');
                    await processOCElement(element);
                });
            } catch (error) {
                logError('Main processing error', error);
            } finally {
                processing = false;
            }
        });
    }, debounceDelay);

    // MutationObserver setup
    function initObserver() {
        if (observer) observer.disconnect();

        observer = new MutationObserver(processOCs);
        observer.observe(document.querySelector('#faction-crimes-root') || document.body, {
            childList: true,
            subtree: true,
            attributes: false,
            characterData: false
        });
    }

    // Initialization
    function initialize() {
        initObserver();
        processOCs();

        // Re-process when OC tabs change
        document.querySelector('.buttonsContainer___aClaa')?.addEventListener('click', () => {
            document.querySelectorAll('.wrapper___U2Ap7.oc-processed').forEach(el => {
                el.classList.remove('oc-processed');
            });
            processOCs();
        });
    }

    // Start script
    if (document.readyState === 'complete') {
        initialize();
    } else {
        window.addEventListener('load', initialize);
    }
})();