OC 2.0 TNL-Forge Role Requirements

Torn OC 2.0 Requirements for Roles in Specific Crimes, based on TNL Forge

// ==UserScript==
// @name         OC 2.0 TNL-Forge Role Requirements
// @namespace    MonChoon_
// @version      2.0
// @description  Torn OC 2.0 Requirements for Roles in Specific Crimes, based on TNL Forge
// @license      MIT
// @author       MonChoon [2250591], Silmaril [2665762]
// @match        https://www.torn.com/factions.php?step=your*
// @run-at       document-idle
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    // Configuration - Replace with your published Google Sheet CSV URL
    // Expected format: Two-row format (Crime row, Role row, CPR row)
    const REQUIREMENTS_CSV_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSb0W9iwm3noNzJVoUArG4VSbeSzpgWlMB9ObhYxU8FdNMzWEhIC852N2SHSWbb-pKFdrBgMwxQr6x-/pub?gid=812446557&single=true&output=csv';

    // Cache settings
    const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes in milliseconds
    const CACHE_KEY = 'oc_crime_requirements';
    const CACHE_TIMESTAMP_KEY = 'oc_crime_requirements_timestamp';

    const CRIMES_TAB = '#/tab=crimes';

    // Updated fallback data
    const FALLBACK_REQUIREMENTS = {
        'Blast from the Past': {'Bomber': 75, 'Engineer': 75, 'Hacker': 70, 'Muscle': 75, 'Picklock #1': 70, 'Picklock #2': 70},
        'Break the Bank': {'Robber': 60, 'Thief #1': 50, 'Thief #2': 65, 'Muscle #1': 60, 'Muscle #2': 60, 'Muscle #3': 65},
        'Stacking the Deck': {'Cat Burglar': 68, 'Driver': 50, 'Imitator': 68, 'Hacker': 68},
        'Ace in the Hole': {'Hacker': 63, 'Driver': 53, 'Imitator': 63, 'Muscle #1': 63, 'Muscle #2': 63},
        'Clinical Precision': {'Cat Burglar': 67, 'Cleaner': 67, 'Imitator': 70, 'Assassin': 67},
        'Bidding War': {'Driver': 75, 'Robber 1': 70, 'Robber 2': 75, 'Robber 3': 75, 'Bomber 1': 70, 'Bomber 2': 75}
    };

    let crimeRequirements = FALLBACK_REQUIREMENTS;

    // Parse CSV data (two-row format: Crime row, Role row, CPR row)
    function parseCSVToRequirements(csvText) {
        const lines = csvText.trim().split('\n');
        const requirements = {};

        // Process in groups of 3 lines (Crime, Role, CPR)
        for (let i = 0; i < lines.length; i += 3) {
            if (i + 2 >= lines.length) break;

            const crimeRow = lines[i].split(',').map(v => v.trim().replace(/^"|"$/g, ''));
            const roleRow = lines[i + 1].split(',').map(v => v.trim().replace(/^"|"$/g, ''));
            const cprRow = lines[i + 2].split(',').map(v => v.trim().replace(/^"|"$/g, ''));

            // First cell should be "Crime", second cell is the crime name
            if (crimeRow[0] !== 'Crime' || !crimeRow[1]) continue;

            const crimeName = crimeRow[1];
            requirements[crimeName] = {};

            // Process roles (skip first column which is "Role" or "CPR")
            for (let j = 1; j < roleRow.length && j < cprRow.length; j++) {
                const roleName = roleRow[j];
                const cprValue = cprRow[j];

                if (roleName && cprValue && !isNaN(cprValue)) {
                    const cpr = parseInt(cprValue);
                    if (cpr >= 0) { // Allow 0 values for roles like "Picklock #2"
                        requirements[crimeName][roleName] = cpr;
                    }
                }
            }
        }

        return requirements;
    }

    // Cache management
    function getCachedRequirements() {
        try {
            const timestamp = localStorage.getItem(CACHE_TIMESTAMP_KEY);
            const cached = localStorage.getItem(CACHE_KEY);

            if (timestamp && cached) {
                const cacheAge = Date.now() - parseInt(timestamp);
                if (cacheAge < CACHE_DURATION) {
                    console.log('OC Requirements: Using cached data');
                    return JSON.parse(cached);
                }
            }
        } catch (e) {
            console.warn('OC Requirements: Error reading cache:', e);
        }
        return null;
    }

    function setCachedRequirements(requirements) {
        try {
            localStorage.setItem(CACHE_KEY, JSON.stringify(requirements));
            localStorage.setItem(CACHE_TIMESTAMP_KEY, Date.now().toString());
        } catch (e) {
            console.warn('OC Requirements: Error setting cache:', e);
        }
    }

    // Fetch requirements from Google Sheets
    function fetchRequirements() {
        return new Promise((resolve) => {
            // Check cache first
            const cached = getCachedRequirements();
            if (cached) {
                resolve(cached);
                return;
            }

            console.log('OC Requirements: Fetching from Google Sheets...');

            GM_xmlhttpRequest({
                method: 'GET',
                url: REQUIREMENTS_CSV_URL,
                timeout: 10000,
                onload: function(response) {
                    try {
                        if (response.status === 200) {
                            const requirements = parseCSVToRequirements(response.responseText);
                            setCachedRequirements(requirements);
                            console.log('OC Requirements: Successfully loaded from Google Sheets');
                            console.log('Loaded crimes:', Object.keys(requirements));
                            resolve(requirements);
                        } else {
                            throw new Error(`HTTP ${response.status}`);
                        }
                    } catch (e) {
                        console.warn('OC Requirements: Error parsing CSV, using fallback:', e);
                        resolve(FALLBACK_REQUIREMENTS);
                    }
                },
                onerror: function(error) {
                    console.warn('OC Requirements: Network error, using fallback:', error);
                    resolve(FALLBACK_REQUIREMENTS);
                },
                ontimeout: function() {
                    console.warn('OC Requirements: Timeout, using fallback');
                    resolve(FALLBACK_REQUIREMENTS);
                }
            });
        });
    }

    // Apply requirements to crime roles
    function applyCrimeRequirements() {
        if (window.location.href.indexOf(CRIMES_TAB) === -1) return;

        document.querySelectorAll('[class^=scenario]').forEach(scenario => {
            const titleElement = scenario.querySelector('[class^=panelTitle___]');
            if (!titleElement) return;

            const crimeTitle = titleElement.textContent.trim();
            const requirements = crimeRequirements[crimeTitle];

            if (!requirements) return;

            scenario.querySelectorAll('[class^=wrapper___] > [class^=wrapper___]').forEach(role => {
                const slotTitleElement = role.querySelector('[class^=slotHeader___] > [class^=title___]');
                const slotSkillElement = role.querySelector('[class^=slotHeader___] > [class^=successChance___]');

                if (!slotTitleElement || !slotSkillElement) return;

                const slotTitle = slotTitleElement.textContent.trim();
                const slotSkill = Number(slotSkillElement.textContent);

                if (role.className.indexOf('waitingJoin___') > -1) {
                    const roleRequirement = requirements[slotTitle];

                    if (roleRequirement !== undefined && slotSkill < roleRequirement) {
                        const roleJoinBtn = role.querySelector('[class^=slotBody___] > [class^=joinContainer___] > [class^=joinButtonContainer___] > [class*=joinButton___]');

                        if (roleJoinBtn && !roleJoinBtn.hasAttribute('data-oc-modified')) {
                            roleJoinBtn.setAttribute('disabled', true);
                            roleJoinBtn.textContent = `<${roleRequirement}`;
                            roleJoinBtn.style.color = 'crimson';
                            roleJoinBtn.style.fontWeight = 'bold';
                            roleJoinBtn.setAttribute('data-oc-modified', 'true');

                            // Add tooltip to show the requirement
                            roleJoinBtn.title = `${slotTitle} requires ${roleRequirement}+ CPR (you have ${slotSkill})`;
                        }
                    }
                }
            });
        });
    }

    // Initialize the script
    async function initialize() {
        console.log('OC Requirements: Initializing...');

        // Load requirements (from cache or fetch)
        crimeRequirements = await fetchRequirements();

        // Set up observer for DOM changes
        const observerTarget = document.querySelector("#faction-crimes");
        if (!observerTarget) {
            console.warn('OC Requirements: Faction crimes element not found');
            return;
        }

        const observerConfig = {
            attributes: false,
            childList: true,
            characterData: false,
            subtree: true
        };

        const observer = new MutationObserver(function(mutations) {
            let shouldApply = false;

            mutations.forEach(mutation => {
                if (String(mutation.target.className).indexOf('description___') > -1) {
                    shouldApply = true;
                }
            });

            if (shouldApply) {
                // Small delay to ensure DOM is ready
                setTimeout(applyCrimeRequirements, 100);
            }
        });

        observer.observe(observerTarget, observerConfig);

        // Apply requirements immediately if we're already on the crimes tab
        if (window.location.href.indexOf(CRIMES_TAB) > -1) {
            setTimeout(applyCrimeRequirements, 500);
        }

        console.log('OC Requirements: Initialized successfully');
    }

    // Force refresh requirements (useful for testing)
    function forceRefresh() {
        localStorage.removeItem(CACHE_KEY);
        localStorage.removeItem(CACHE_TIMESTAMP_KEY);
        console.log('OC Requirements: Cache cleared, refreshing...');
        initialize();
    }

    // Expose refresh function to console for debugging
    window.ocRefreshRequirements = forceRefresh;

    // Start the script
    initialize();

})();