您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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== // 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 = 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'; // 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': 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; let observer = null; // 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 functions 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); } }); }); } // Set up the mutation observer for dynamic content function setupObserver() { 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 }; 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){ let crimeParentRow = mutation.parentNode.parentNode.parentNode; let crimeTitle = crimeParentRow.querySelector('[class^=scenario] > [class^=wrapper___] > [class^=panel___] > [class^=panelTitle___]').textContent; let crimeTitleRequirements = crimeRequirements[crimeTitle]; if (crimeTitleRequirements === undefined) return; crimeParentRow.querySelectorAll('[class^=wrapper___] > [class^=wrapper___]').forEach(crime => { let slotTitle = crime.querySelector('[class^=slotHeader___] > [class^=title___]').textContent; let slotSkill = Number(crime.querySelector('[class^=slotHeader___] > [class^=successChance___]').textContent); if (crime.className.indexOf('waitingJoin___') > -1){ let roleRequirement = crimeTitleRequirements[slotTitle]; if (roleRequirement !== undefined){ if (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'); } } } } }); } } }); }); observer.observe(observerTarget, observerConfig); console.log('OC Requirements: Observer set up successfully'); } // Apply requirements to existing crimes on page load function applyToExistingCrimes() { if (window.location.href.indexOf(CRIMES_TAB) === -1) return; // Find all crime containers and process them like the observer does document.querySelectorAll('[class^=scenario]').forEach(scenario => { try { // Use the exact same selector as the observer let crimeTitle = scenario.querySelector('[class^=wrapper___] > [class^=panel___] > [class^=panelTitle___]').textContent; let crimeTitleRequirements = crimeRequirements[crimeTitle]; if (crimeTitleRequirements === undefined) return; // Get the parent container (equivalent to crimeParentRow in observer) let crimeParentRow = scenario.parentNode || scenario; crimeParentRow.querySelectorAll('[class^=wrapper___] > [class^=wrapper___]').forEach(crime => { let slotTitle = crime.querySelector('[class^=slotHeader___] > [class^=title___]').textContent; let slotSkill = Number(crime.querySelector('[class^=slotHeader___] > [class^=successChance___]').textContent); if (crime.className.indexOf('waitingJoin___') > -1){ let roleRequirement = crimeTitleRequirements[slotTitle]; if (roleRequirement !== undefined){ if (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) { console.log('Error processing crime on page load:', e); } }); } // Debug function to show current state function debugInfo() { console.log('=== OC Requirements Debug Info ==='); console.log('Script loaded:', true); console.log('Current URL:', window.location.href); console.log('On crimes tab:', window.location.href.indexOf(CRIMES_TAB) > -1); console.log('Cache timestamp:', localStorage.getItem(CACHE_TIMESTAMP_KEY)); console.log('Requirements loaded:', Object.keys(crimeRequirements).length, 'crimes'); console.log('Requirements:', crimeRequirements); console.log('Faction crimes element exists:', !!document.querySelector("#faction-crimes")); // Test selectors console.log('Found scenarios:', document.querySelectorAll('[class^=scenario]').length); console.log('Found join buttons:', document.querySelectorAll('[class*=joinButton___]').length); console.log('Found waiting join slots:', document.querySelectorAll('[class*=waitingJoin___]').length); const cached = getCachedRequirements(); if (cached) { console.log('Cached data available:', Object.keys(cached).length, 'crimes'); } else { console.log('No cached data'); } console.log('==================================='); } // Force refresh requirements (useful for testing) function forceRefresh() { localStorage.removeItem(CACHE_KEY); localStorage.removeItem(CACHE_TIMESTAMP_KEY); console.log('OC Requirements: Cache cleared, refreshing...'); // Re-initialize to fetch fresh data initialize(); } // Test function function testScript() { console.log('OC Requirements script is loaded and working!'); debugInfo(); } // Main initialization function async function initialize() { console.log('OC Requirements: Initializing...'); // Load requirements (from cache or fetch) crimeRequirements = await fetchRequirements(); // Set up observer for DOM changes setupObserver(); // Apply requirements immediately if we're already on the crimes tab if (window.location.href.indexOf(CRIMES_TAB) > -1) { setTimeout(applyToExistingCrimes, 500); } console.log('OC Requirements: Initialized successfully'); } // Expose functions to console for debugging window.ocRefreshRequirements = forceRefresh; window.ocDebugInfo = debugInfo; window.ocTest = testScript; // Log that the script has loaded console.log('OC Requirements: Script loaded successfully'); console.log('Available debug commands: ocTest(), ocDebugInfo(), ocRefreshRequirements()'); // Start the script initialize();