Torn OC Role Evaluator

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 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');
})();