Torn OC Role Evaluator

Evaluate OC roles based on influence and success chance (fix for finished OCs)

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Torn OC Role Evaluator
// @version      1.14
// @description  Evaluate OC roles based on influence and success chance (fix for finished OCs)
// @author       underko[3362751]
// @match        https://www.torn.com/factions.php*
// @grant        none
// @require      https://update.greasyfork.org/scripts/502635/1422102/waitForKeyElements-CoeJoder-fork.js
// @license      MIT
// @namespace https://greasyfork.org/users/1494305
// ==/UserScript==

(function () {
    'use strict';

    const influenceThresholds = {
        high: { good: 75, ok: 60 },
        medium: { good: 60, ok: 45 },
        low: { good: 40, ok: 30 }
    };

    const ocRoleInfluence = {
        "Pet Project": [
            { role: "Kidnapper",   influence: 41.14 },
            { role: "Muscle",      influence: 26.83 },
            { role: "Picklock",    influence: 32.03 }
        ],
        "Mob Mentality": [
            { role: "Looter #1",   influence: 34.83 },
            { role: "Looter #2",   influence: 25.97 },
            { role: "Looter #3",   influence: 19.87 },
            { role: "Looter #4",   influence: 19.33 }
        ],
        "Cash Me if You Can": [
            { role: "Thief #1",    influence: 46.67 },
            { role: "Thief #2",    influence: 21.87 },
            { role: "Lookout",     influence: 31.46 }
        ],
        "Best of the Lot": [
            { role: "Picklock",    influence: 23.65 },
            { role: "Car Thief",   influence: 21.06 },
            { role: "Muscle",      influence: 36.43 },
            { role: "Imitator",    influence: 18.85 }
        ],
        "Market Forces": [
            { role: "Enforcer",    influence: 27.56 },
            { role: "Negotiator",  influence: 25.59 },
            { role: "Lookout",     influence: 19.05 },
            { role: "Arsonist",    influence:  4.12 },
            { role: "Muscle",      influence: 23.68 }
        ],
        "Smoke and Wing Mirrors": [
            { role: "Car Thief",   influence: 48.20 },
            { role: "Imitator",    influence: 26.30 },
            { role: "Hustler #1",  influence:  7.70 },
            { role: "Hustler #2",  influence: 17.81 }
        ],
        "Gaslight the Way": [
            { role: "Imitator #1", influence:  7.54 },
            { role: "Imitator #2", influence: 34.85 },
            { role: "Imitator #3", influence: 40.25 },
            { role: "Looter #1",   influence:  7.54 },
            { role: "Looter #2",   influence:  0.00 },
            { role: "Looter #3",   influence:  9.83 }
        ],
        "Stage Fright": [
            { role: "Enforcer",    influence: 16.89 },
            { role: "Muscle #1",   influence: 21.92 },
            { role: "Muscle #2",   influence:  2.09 },
            { role: "Muscle #3",   influence:  9.49 },
            { role: "Lookout",     influence:  7.68 },
            { role: "Sniper",      influence: 41.92 }
        ],
        "Snow Blind": [
            { role: "Hustler",     influence: 51.40 },
            { role: "Imitator",    influence: 30.44 },
            { role: "Muscle #1",   influence:  9.08 },
            { role: "Muscle #2",   influence:  9.08 }
        ],
        "Leave No Trace": [
            { role: "Techie",      influence: 24.40 },
            { role: "Negotiator",  influence: 29.07 },
            { role: "Imitator",    influence: 46.54 }
        ],
        "No Reserve": [
            { role: "Car Thief",   influence: 30.86 },
            { role: "Techie",      influence: 37.88 },
            { role: "Engineer",    influence: 31.27 }
        ],
        "Counter Offer": [
            { role: "Robber",      influence: 33.29 },
            { role: "Looter",      influence:  4.69 },
            { role: "Hacker",      influence: 16.72 },
            { role: "Picklock",    influence: 17.10 },
            { role: "Engineer",    influence: 28.21 }
        ],
        "Guardian Ángels": [
            { role: "Enforcer",    influence: 28.34 },
            { role: "Hustler",     influence: 37.72 },
            { role: "Engineer",    influence: 33.93 }
        ],
        "Honey Trap": [
            { role: "Enforcer",    influence: 20.21 },
            { role: "Muscle #1",   influence: 34.32 },
            { role: "Muscle #2",   influence: 45.47 }
        ],
        "Bidding War": [
            { role: "Robber #1",   influence:  6.82 },
            { role: "Driver",      influence: 21.93 },
            { role: "Robber #2",   influence: 19.63 },
            { role: "Robber #3",   influence: 25.65 },
            { role: "Bomber #1",   influence: 10.96 },
            { role: "Bomber #2",   influence: 15.00 }
        ],
        "Sneaky Git Grab": [
            { role: "Imitator",    influence: 12.48 },
            { role: "Pickpocket",  influence: 43.77 },
            { role: "Hacker",      influence: 23.11 },
            { role: "Techie",      influence: 20.64 }
        ],
        "Blast from the Past": [
            { role: "Picklock #1", influence:  9.81 },
            { role: "Hacker",      influence:  6.18 },
            { role: "Engineer",    influence: 25.29 },
            { role: "Bomber",      influence: 20.40 },
            { role: "Muscle",      influence: 36.75 },
            { role: "Picklock #2", influence:  1.56 }
        ],
        "Break the Bank": [
            { role: "Robber",      influence: 10.84 },
            { role: "Muscle #1",   influence: 10.27 },
            { role: "Muscle #2",   influence:  7.78 },
            { role: "Thief #1",    influence:  3.55 },
            { role: "Muscle #3",   influence: 33.54 },
            { role: "Thief #2",    influence: 34.03 }
        ],
        "Stacking the Deck": [
            { role: "Cat Burglar", influence: 31.99 },
            { role: "Driver",      influence:  3.86 },
            { role: "Hacker",      influence: 25.64 },
            { role: "Imitator",    influence: 38.52 }
        ],
        "Clinical Precision": [
            { role: "Imitator",    influence: 41.51 },
            { role: "Cat Burglar", influence: 22.21 },
            { role: "Assassin",    influence: 14.56 },
            { role: "Cleaner",     influence: 21.71 }
        ],
        "Ace in the Hole": [
            { role: "Imitator",    influence: 13.73 },
            { role: "Muscle #1",   influence: 18.55 },
            { role: "Muscle #2",   influence: 18.88 },
            { role: "Hacker",      influence: 37.49 },
            { role: "Driver",      influence: 11.35 }
        ]
    };

    const PROCESSED_ATTR = 'data-oc-evaluated';

    // Build a lookup for inferring crime from roles
    function buildRoleSignatures() {
        const signatures = {};
        for (const [crimeName, roles] of Object.entries(ocRoleInfluence)) {
            // Create a signature from sorted role names
            const sig = roles.map(r => r.role).sort().join('|');
            signatures[sig] = crimeName;
        }
        return signatures;
    }

    const roleSignatures = buildRoleSignatures();

    function inferCrimeFromRoles(roleNames) {
        const sortedRoles = [...roleNames].sort().join('|');
        
        // Exact match
        if (roleSignatures[sortedRoles]) {
            return roleSignatures[sortedRoles];
        }

        // Partial match - find best match
        let bestMatch = null;
        let bestScore = 0;

        for (const [crimeName, roles] of Object.entries(ocRoleInfluence)) {
            const crimeRoles = roles.map(r => r.role);
            let matches = 0;
            for (const role of roleNames) {
                if (crimeRoles.includes(role)) matches++;
            }
            const score = matches / Math.max(roleNames.length, crimeRoles.length);
            if (score > bestScore && score >= 0.5) {
                bestScore = score;
                bestMatch = crimeName;
            }
        }

        return bestMatch;
    }

    function classifyOcRoleInfluence(ocName, roleName, successChance) {
        const roleData = ocRoleInfluence[ocName]?.find(r => r.role === roleName);
        const influence = roleData ? roleData.influence : null;

        if (influence === null) {
            return { evaluation: 'unknown', influence: null };
        }

        let thresholds;
        if (influence >= 30) thresholds = influenceThresholds.high;
        else if (influence >= 10) thresholds = influenceThresholds.medium;
        else thresholds = influenceThresholds.low;

        if (successChance >= thresholds.good) return { evaluation: 'good', influence };
        if (successChance >= thresholds.ok) return { evaluation: 'ok', influence };
        return { evaluation: 'bad', influence };
    }

    function findCrimeTitleFromElement(element) {
        // Strategy 1: Look up the DOM tree (up to 30 levels)
        let parent = element;
        let depth = 0;
        while (parent && depth < 30) {
            // Check for panel title in this element
            const titleEl = parent.querySelector('[class*="panelTitle"]');
            if (titleEl) {
                return titleEl.textContent.trim();
            }

            // Check siblings
            if (parent.previousElementSibling) {
                const sibTitleEl = parent.previousElementSibling.querySelector('[class*="panelTitle"]');
                if (sibTitleEl) {
                    return sibTitleEl.textContent.trim();
                }
                // Check if sibling itself is the title
                if (parent.previousElementSibling.className?.includes('panelTitle')) {
                    return parent.previousElementSibling.textContent.trim();
                }
            }

            parent = parent.parentElement;
            depth++;
        }

        return null;
    }

    function findCrimeWrapper(slotHeader) {
        // Find the wrapper that contains all the roles for this crime
        let element = slotHeader.parentElement;
        let depth = 0;

        while (element && depth < 15) {
            // Look for wrapper with notOpening or similar class patterns
            const className = element.className || '';
            if (className.includes('wrapper') && 
                (className.includes('notOpening') || 
                 className.includes('Opening') ||
                 className.includes('crime'))) {
                return element;
            }

            // Check if this element contains multiple slotHeaders
            const headers = element.querySelectorAll('[class*="slotHeader"]');
            if (headers.length > 1) {
                return element;
            }

            element = element.parentElement;
            depth++;
        }

        return slotHeader.parentElement?.parentElement;
    }

    function processSlotHeader(slotHeader) {
        if (slotHeader.hasAttribute(PROCESSED_ATTR)) return;

        // Find role title
        const roleEl = slotHeader.querySelector('[class*="title"]');
        if (!roleEl) return;

        const roleName = roleEl.textContent.trim();

        // Find success chance
        const successEl = slotHeader.querySelector('[class*="successChance"]');
        if (!successEl) return;

        const originalText = successEl.textContent.trim();
        if (originalText.includes('/')) {
            slotHeader.setAttribute(PROCESSED_ATTR, 'true');
            return;
        }

        const chance = parseInt(originalText, 10);
        if (isNaN(chance)) return;

        // Try to find crime title
        let crimeTitle = findCrimeTitleFromElement(slotHeader);

        // If no title found, try to infer from all roles in the same crime wrapper
        if (!crimeTitle) {
            const crimeWrapper = findCrimeWrapper(slotHeader);
            if (crimeWrapper) {
                const allRoleEls = crimeWrapper.querySelectorAll('[class*="title"]');
                const roleNames = [];
                allRoleEls.forEach(el => {
                    const text = el.textContent.trim();
                    // Filter out non-role titles (check if it looks like a role name)
                    if (text && !text.includes(' ') || text.includes('#')) {
                        roleNames.push(text);
                    }
                });

                if (roleNames.length > 0) {
                    crimeTitle = inferCrimeFromRoles(roleNames);
                    if (crimeTitle) {
                        console.log('[OC Evaluator] Inferred crime:', crimeTitle, 'from roles:', roleNames);
                    }
                }
            }
        }

        if (!crimeTitle) {
            console.log('[OC Evaluator] Could not determine crime for role:', roleName);
            return;
        }

        const evaluation = classifyOcRoleInfluence(crimeTitle, roleName, chance);

        if (evaluation.influence !== null) {
            successEl.textContent = `${chance}/${Math.round(evaluation.influence)}`;
        }

        switch (evaluation.evaluation) {
            case 'good':
                slotHeader.style.backgroundColor = '#239b56';
                break;
            case 'ok':
                slotHeader.style.backgroundColor = '#ca6f1e';
                break;
            case 'bad':
                slotHeader.style.backgroundColor = '#a93226';
                break;
        }

        slotHeader.setAttribute(PROCESSED_ATTR, 'true');
    }

    function processAllCrimes() {
        // Find all slot headers
        const allSlotHeaders = document.querySelectorAll('[class*="slotHeader"]');
        allSlotHeaders.forEach(slotHeader => {
            try {
                processSlotHeader(slotHeader);
            } catch (e) {
                console.error('[OC Evaluator] Error processing slot:', e);
            }
        });
    }

    function setupMutationObserver(root) {
        if (root.hasAttribute('data-oc-observer-attached')) return;
        root.setAttribute('data-oc-observer-attached', 'true');

        const observer = new MutationObserver(() => {
            clearTimeout(window.ocEvalTimeout);
            window.ocEvalTimeout = setTimeout(processAllCrimes, 150);
        });

        observer.observe(root, { childList: true, subtree: true });
        console.log('[OC Evaluator] Observer attached to:', root.id || root.className);
    }

    function init() {
        // Set up observer on the crimes root or body
        const root = document.querySelector("#faction-crimes-root") || 
                     document.querySelector('[class*="crimes"]') ||
                     document.body;
        
        setupMutationObserver(root);
        processAllCrimes();
    }

    // Use waitForKeyElements for initial load
    waitForKeyElements("#faction-crimes-root", (root) => {
        console.log('[OC Evaluator] Found faction-crimes-root');
        setupMutationObserver(root);
        processAllCrimes();
    });

    // Also set up observers on slot headers directly as they appear
    waitForKeyElements('[class*="slotHeader"]', (slotHeader) => {
        setTimeout(() => processSlotHeader(slotHeader), 100);
    });

    // Fallback initialization
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

    window.addEventListener('load', () => setTimeout(init, 500));

    // Polling fallback
    setInterval(processAllCrimes, 2000);

    // Handle tab clicks
    document.addEventListener('click', (e) => {
        if (e.target.closest('[class*="tab"]') || e.target.closest('button')) {
            setTimeout(processAllCrimes, 300);
            setTimeout(processAllCrimes, 800);
        }
    }, true);

    console.log('[OC Evaluator] Script loaded v1.13');
})();