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