您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Torn OC 2.0 Requirements for Roles in Specific Crimes, based on TNL Forge
// ==UserScript== // @name [FORGE] OC 2.0 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();