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.1
// @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
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      docs.google.com
// @connect      googleusercontent.com
// @connect      doc-*.sheets.googleusercontent.com
// ==/UserScript==

// Configuration and global variables
const REQUIREMENTS_CSV_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSb0W9iwm3noNzJVoUArG4VSbeSzpgWlMB9ObhYxU8FdNMzWEhIC852N2SHSWbb-pKFdrBgMwxQr6x-/pub?gid=812446557&single=true&output=csv';
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes for testing
const CRIMES_TAB = '#/tab=crimes';

// Fallback data in case Google Sheets is unavailable
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': 80, '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;
let observer = null;
let isInitialized = false;

// Enhanced storage functions with GM fallback
function getStoredValue(key, defaultValue = null) {
    try {
        // Try GM_getValue first (more reliable on mobile)
        if (typeof GM_getValue !== 'undefined') {
            return GM_getValue(key, defaultValue);
        }
        // Fallback to localStorage
        const stored = localStorage.getItem(key);
        return stored ? JSON.parse(stored) : defaultValue;
    } catch (e) {
        return defaultValue;
    }
}

function setStoredValue(key, value) {
    try {
        // Try GM_setValue first (more reliable on mobile)
        if (typeof GM_setValue !== 'undefined') {
            GM_setValue(key, value);
            return;
        }
        // Fallback to localStorage
        localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
        // Silent fail
    }
}

// Parse CSV data with detailed logging
function parseCSVToRequirements(csvText) {
    console.log('OC Requirements: Starting CSV parsing...');
    console.log('OC Requirements: CSV text length =', csvText.length);

    try {
        const lines = csvText.trim().split('\n');
        console.log('OC Requirements: Found', lines.length, 'lines in CSV');

        if (lines.length < 3) {
            console.log('OC Requirements: Not enough lines for parsing (need at least 3)');
            return null;
        }

        const requirements = {};
        let groupsProcessed = 0;

        // Process in groups of 3 lines (Crime, Role, CPR)
        for (let i = 0; i < lines.length; i += 3) {
            if (i + 2 >= lines.length) {
                console.log(`OC Requirements: Stopping at line ${i}, not enough lines for complete group`);
                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, ''));

            console.log(`OC Requirements: Processing group ${groupsProcessed + 1}:`);
            console.log(`  Crime row (${crimeRow.length} cols):`, crimeRow.slice(0, 3), '...');
            console.log(`  Role row (${roleRow.length} cols):`, roleRow.slice(0, 3), '...');
            console.log(`  CPR row (${cprRow.length} cols):`, cprRow.slice(0, 3), '...');

            // First cell should be "Crime", second cell is the crime name
            if (crimeRow[0] !== 'Crime' || !crimeRow[1]) {
                console.log(`  Skipping - invalid crime row: "${crimeRow[0]}", "${crimeRow[1]}"`);
                continue;
            }

            const crimeName = crimeRow[1];
            console.log(`  Processing crime: "${crimeName}"`);
            requirements[crimeName] = {};
            let rolesAdded = 0;

            // 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) {
                        requirements[crimeName][roleName] = cpr;
                        rolesAdded++;
                    }
                }
            }

            console.log(`  Added ${rolesAdded} roles for "${crimeName}"`);
            groupsProcessed++;
        }

        console.log(`OC Requirements: Parsing complete - processed ${groupsProcessed} crime groups`);
        console.log('OC Requirements: Final crimes found:', Object.keys(requirements));

        return Object.keys(requirements).length > 0 ? requirements : null;

    } catch (e) {
        console.log('OC Requirements: CSV parsing error =', e.message);
        console.log('OC Requirements: Error stack =', e.stack);
        return null;
    }
}

// Enhanced cache management
function getCachedRequirements() {
    try {
        const timestamp = getStoredValue('oc_cache_timestamp', 0);
        const cached = getStoredValue('oc_cache_data', null);

        if (timestamp && cached && (Date.now() - timestamp) < CACHE_DURATION) {
            console.log('OC Requirements: Using cached data');
            return cached;
        }
    } catch (e) {
        // Silent fail
    }
    return null;
}

function setCachedRequirements(requirements) {
    try {
        setStoredValue('oc_cache_timestamp', Date.now());
        setStoredValue('oc_cache_data', requirements);
    } catch (e) {
        // Silent fail
    }
}

// Enhanced fetch with detailed error logging
function fetchRequirements() {
    return new Promise((resolve) => {
        // Check cache first
        const cached = getCachedRequirements();
        if (cached) {
            console.log('OC Requirements: Using cached data - success');
            resolve(cached);
            return;
        }

        console.log('OC Requirements: Starting fetch from Google Sheets...');
        console.log('OC Requirements: URL =', REQUIREMENTS_CSV_URL);

        // Method 1: Try GM_xmlhttpRequest with detailed logging
        if (typeof GM_xmlhttpRequest !== 'undefined') {
            console.log('OC Requirements: Attempting GM_xmlhttpRequest...');

            GM_xmlhttpRequest({
                method: 'GET',
                url: REQUIREMENTS_CSV_URL,
                timeout: 10000,
                headers: {
                    'User-Agent': 'Mozilla/5.0 (compatible; userscript)',
                    'Accept': 'text/csv,text/plain,*/*'
                },
                onload: function(response) {
                    console.log('OC Requirements: GM_xmlhttpRequest response received');
                    console.log('OC Requirements: Status =', response.status);
                    console.log('OC Requirements: Response length =', response.responseText ? response.responseText.length : 0);

                    try {
                        if (response.status === 200 && response.responseText) {
                            console.log('OC Requirements: Starting CSV parsing...');
                            console.log('OC Requirements: First 200 chars =', response.responseText.substring(0, 200));

                            const requirements = parseCSVToRequirements(response.responseText);
                            if (requirements) {
                                console.log('OC Requirements: CSV parsing successful');
                                console.log('OC Requirements: Found crimes =', Object.keys(requirements));
                                setCachedRequirements(requirements);
                                resolve(requirements);
                                return;
                            } else {
                                console.log('OC Requirements: CSV parsing returned null/empty');
                            }
                        } else {
                            console.log('OC Requirements: Bad status or empty response');
                        }
                        throw new Error(`GM_xmlhttpRequest failed: status ${response.status}`);
                    } catch (e) {
                        console.log('OC Requirements: GM_xmlhttpRequest processing error =', e.message);
                        tryFetchFallback(resolve);
                    }
                },
                onerror: function(error) {
                    console.log('OC Requirements: GM_xmlhttpRequest network error =', error);
                    console.log('OC Requirements: This might be a CORS permission issue!');
                    console.log('OC Requirements: Check if Tampermonkey asked for docs.google.com access permission');
                    tryFetchFallback(resolve);
                },
                ontimeout: function() {
                    console.log('OC Requirements: GM_xmlhttpRequest timeout');
                    tryFetchFallback(resolve);
                }
            });
        } else {
            console.log('OC Requirements: GM_xmlhttpRequest not available');
            tryFetchFallback(resolve);
        }
    });
}

// Fallback method using regular fetch with detailed logging
function tryFetchFallback(resolve) {
    console.log('OC Requirements: Trying fetch fallback...');

    if (typeof fetch !== 'undefined') {
        console.log('OC Requirements: fetch() is available, attempting request...');

        fetch(REQUIREMENTS_CSV_URL, {
            method: 'GET',
            mode: 'cors',
            cache: 'no-cache'
        })
        .then(response => {
            console.log('OC Requirements: fetch response received');
            console.log('OC Requirements: fetch status =', response.status);
            console.log('OC Requirements: fetch ok =', response.ok);

            if (response.ok) {
                return response.text();
            }
            throw new Error(`Fetch failed with status ${response.status}`);
        })
        .then(text => {
            console.log('OC Requirements: fetch text received, length =', text.length);
            console.log('OC Requirements: fetch first 200 chars =', text.substring(0, 200));

            const requirements = parseCSVToRequirements(text);
            if (requirements) {
                console.log('OC Requirements: fetch + parsing successful');
                console.log('OC Requirements: found crimes =', Object.keys(requirements));
                setCachedRequirements(requirements);
                resolve(requirements);
                return;
            } else {
                console.log('OC Requirements: fetch parsing returned null/empty');
            }
            throw new Error('Fetch parsing failed');
        })
        .catch(error => {
            console.log('OC Requirements: fetch method failed =', error.message);
            console.log('OC Requirements: Using fallback data due to fetch failure');
            resolve(FALLBACK_REQUIREMENTS);
        });
    } else {
        console.log('OC Requirements: fetch() not available, using fallback data');
        resolve(FALLBACK_REQUIREMENTS);
    }
}

// Set up the mutation observer for dynamic content
function setupObserver() {
    const observerTarget = document.querySelector("#faction-crimes");
    if (!observerTarget) {
        return false;
    }

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

    observer = new MutationObserver(function(mutations) {
        mutations.forEach(mutationRaw => {
            if (window.location.href.indexOf(CRIMES_TAB) > -1){
                let mutation = mutationRaw.target;
                if (String(mutation.className).indexOf('description___') > -1){
                    processSpecificCrime(mutation);
                }
            }
        });
    });

    observer.observe(observerTarget, observerConfig);
    return true;
}

// Process a specific crime when it's detected by the observer
function processSpecificCrime(mutation) {
    try {
        let crimeParentRow = mutation.parentNode.parentNode.parentNode;
        let crimeTitleElement = crimeParentRow.querySelector('[class^=scenario] > [class^=wrapper___] > [class^=panel___] > [class^=panelTitle___]');

        if (!crimeTitleElement) return;

        let crimeTitle = crimeTitleElement.textContent;
        let crimeTitleRequirements = crimeRequirements[crimeTitle];
        if (crimeTitleRequirements === undefined) return;

        crimeParentRow.querySelectorAll('[class^=wrapper___] > [class^=wrapper___]').forEach(crime => {
            processCrimeRole(crime, crimeTitleRequirements);
        });
    } catch (e) {
        // Silent error handling
    }
}

// Process individual crime role
function processCrimeRole(crime, crimeTitleRequirements) {
    try {
        let slotTitleElement = crime.querySelector('[class^=slotHeader___] > [class^=title___]');
        let slotSkillElement = crime.querySelector('[class^=slotHeader___] > [class^=successChance___]');

        if (!slotTitleElement || !slotSkillElement) return;

        let slotTitle = slotTitleElement.textContent;
        let slotSkill = Number(slotSkillElement.textContent);

        if (crime.className.indexOf('waitingJoin___') > -1){
            let roleRequirement = crimeTitleRequirements[slotTitle];
            if (roleRequirement !== undefined && slotSkill < roleRequirement){
                let roleJoinBtn = crime.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.setAttribute('data-oc-modified', 'true');
                }
            }
        }
    } catch (e) {
        // Silent error handling
    }
}

// Apply requirements to existing crimes on page load
function applyToExistingCrimes() {
    if (window.location.href.indexOf(CRIMES_TAB) === -1) return;

    document.querySelectorAll('[class^=scenario]').forEach(scenario => {
        try {
            let crimeTitleElement = scenario.querySelector('[class^=wrapper___] > [class^=panel___] > [class^=panelTitle___]');
            if (!crimeTitleElement) return;

            let crimeTitle = crimeTitleElement.textContent;
            let crimeTitleRequirements = crimeRequirements[crimeTitle];
            if (crimeTitleRequirements === undefined) return;

            let crimeParentRow = scenario.parentNode || scenario;
            crimeParentRow.querySelectorAll('[class^=wrapper___] > [class^=wrapper___]').forEach(crime => {
                processCrimeRole(crime, crimeTitleRequirements);
            });
        } catch (e) {
            // Silent error handling
        }
    });
}

// Force refresh function with detailed logging
function forceRefresh() {
    try {
        console.log('OC Requirements: Forcing refresh...');
        setStoredValue('oc_cache_timestamp', 0);
        console.log('OC Requirements: Cache cleared');
        initialize();
    } catch (e) {
        console.log('OC Requirements: Error during refresh =', e.message);
    }
}

// Diagnostic function for troubleshooting
function runDiagnostics() {
    console.log('=== OC REQUIREMENTS DIAGNOSTICS ===');
    console.log('Current URL:', window.location.href);
    console.log('On crimes tab:', window.location.href.indexOf(CRIMES_TAB) > -1);
    console.log('GM_xmlhttpRequest available:', typeof GM_xmlhttpRequest !== 'undefined');
    console.log('fetch available:', typeof fetch !== 'undefined');
    console.log('CSV URL:', REQUIREMENTS_CSV_URL);

    // Test storage
    try {
        setStoredValue('test_key', 'test_value');
        const testValue = getStoredValue('test_key', null);
        console.log('Storage test:', testValue === 'test_value' ? 'PASS' : 'FAIL');
    } catch (e) {
        console.log('Storage test: FAIL -', e.message);
    }

    // Test CSV loading
    console.log('Testing CSV fetch...');
    fetchRequirements().then(result => {
        console.log('Fetch test result:', result === FALLBACK_REQUIREMENTS ? 'USED_FALLBACK' : 'SUCCESS');
        console.log('Requirements loaded:', Object.keys(result).length, 'crimes');
        console.log('=== DIAGNOSTICS COMPLETE ===');
    }).catch(e => {
        console.log('Fetch test: ERROR -', e.message);
        console.log('=== DIAGNOSTICS COMPLETE ===');
    });
}

// Main initialization function
async function initialize() {
    if (isInitialized) return;

    try {
        // Load requirements
        crimeRequirements = await fetchRequirements();

        // Set up observer
        const observerSet = setupObserver();

        // Apply to existing crimes if on the crimes tab
        if (window.location.href.indexOf(CRIMES_TAB) > -1) {
            setTimeout(applyToExistingCrimes, 1000);
        }

        isInitialized = true;
        console.log('OC Requirements: Initialized successfully');

    } catch (e) {
        console.log('OC Requirements: Initialization failed, using fallback data');
        crimeRequirements = FALLBACK_REQUIREMENTS;
        setupObserver();
        if (window.location.href.indexOf(CRIMES_TAB) > -1) {
            setTimeout(applyToExistingCrimes, 1000);
        }
        isInitialized = true;
    }
}

// Expose functions for testing and debugging
window.ocRefreshRequirements = forceRefresh;
window.ocDiagnostics = runDiagnostics;

console.log('OC Requirements: Script loaded');
console.log('Available commands: ocRefreshRequirements(), ocDiagnostics()');

// Start the script
initialize();
// initialize();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址