Torn OC Role Evaluator

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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');
})();