// ==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();