您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Show value per nerve on the crime pages
// ==UserScript== // @name Crime Profitability // @namespace heartflower.torn // @version 1.2 // @description Show value per nerve on the crime pages // @author Heartflower [2626587] // @match https://www.torn.com/loader.php?sid=crimes* // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com // @grant GM.xmlHttpRequest // ==/UserScript== (function() { 'use strict'; console.log('[HF] Crime Profatibility script running'); let emforusData = `https://docs.google.com/spreadsheets/d/13wUFhhssuPdAONI_OmRJi6l_Bs7KRZXDgVFCn7uJJNQ/gviz/tq?tqx=out:csv&gid=560321570`; let crackingData = `https://docs.google.com/spreadsheets/d/13wUFhhssuPdAONI_OmRJi6l_Bs7KRZXDgVFCn7uJJNQ/gviz/tq?tqx=out:csv&gid=1626436424`; let rememberedData = JSON.parse(localStorage.getItem('hf-crime-profitability-data')); let rememberedCrackingData = JSON.parse(localStorage.getItem('hf-crime-profitability-cracking-data')); let lastFetched = Number(localStorage.getItem('hf-crime-profitability-last-fetched')); let cachedData = null; let cachedCrackingData = null; let currentHref = window.location.href; let settings = {}; let savedSettings = JSON.parse(localStorage.getItem('hf-crime-profitability-settings')); if (savedSettings) settings = savedSettings; let threshold = 0; let savedThreshold = Number(localStorage.getItem('hf-crime-profitability-threshold')); if (savedThreshold) threshold = savedThreshold; // MAKE PDA COMPATIBLE let pda = ('xmlhttpRequest' in GM); let httpRequest = pda ? 'xmlhttpRequest' : 'xmlHttpRequest'; // Display value per nerve on search for most crimes function crimePage(data, crackingData, observed, retries = 30) { let list = document.body.querySelector('.virtualList___noLef'); let subCrimes = list?.querySelectorAll('.crimeOptionSection___hslpu'); // If DOM isn't fully loaded, try again if (!list || !subCrimes || subCrimes.length < 2) { if (retries > 0) { setTimeout(() => crimePage(data, crackingData, observed, retries - 1), 100); } else { console.warn('[HF] Gave up looking for Crime Page subtitles after 30 retries.'); } return; } let maximum = -Infinity, maximumElement = null; // SPECIAL CHANGES FOR SPECIFIC CRIMES // // Create an observer on the pickpocketing page, as new targets are added and old ones removed if (window.location.href.includes('pickpocketing') && !observed) createObserver(list, data); // Create an observer on the cracking page for scrolling reasons if (window.location.href.includes('cracking') && !observed) createObserver(list, data, crackingData); // FORGERY: dropdown container on top of the page let forgeryPage = window.location.href.includes('forgery'); let maximumForgery = -Infinity, maximumForgeryTarget = null; // CRACKING: check BFS as $/N differs on it let bfs = 0; let cracking = document.body.querySelector('.strength___DM3lW .value___FmWPr'); if (cracking) bfs = Math.round(parseFloat(cracking.textContent.trim())); // BACK TO THE MAIN CODE // // Create a datamap for each subcrime let dataMap = new Map(); for (let crimeData of data) { if (crimeData.target === 'City Centre') crimeData.target = 'City Center'; // Issue in the sheet for graffiti if (crimeData.target) dataMap.set(crimeData.target.toLowerCase(), crimeData); // If forgery, find the best forgery target! if (forgeryPage && crimeData.crime === 'Forgery' && parseInt(crimeData['$/N']?.replace(/[$,]/g, '')) > maximumForgery) { maximumForgery = parseInt(crimeData['$/N']?.replace(/[$,]/g, '')); maximumForgeryTarget = crimeData.target; } } // SPECIAL CHANGES FOR SPECIFIC CRIMES // // Create a CRACKING separate datamap due to separate data sheet let crackingDataMap = new Map(); if (cracking && crackingData) { for (let crimeData of crackingData) { if (crimeData['Targeted service ']) crackingDataMap.set(crimeData['Targeted service '].toLowerCase(), crimeData); } } // BACK TO THE MAIN CODE // for (let subCrime of subCrimes) { let firstTextNode = Array.from(subCrime.childNodes).find(node => node.nodeType === Node.TEXT_NODE); let label = firstTextNode?.textContent.trim().toLowerCase(); // SPECIAL CHANGES FOR SPECIFIC CRIMES // // PICKPOCKETING let pickpocketing = subCrime.querySelector('.titleAndProps___DdeVu div'); if (pickpocketing) label = pickpocketing.textContent.trim().toLowerCase(); // CRACKING cracking = subCrime.querySelector('.type___T9oMA'); let crackingService = subCrime.querySelector('.service___uYhDL'); // FORGERY (MOBILE) let mobileForgery = subCrime.querySelector('.tabletProjectTitle___Wsdf7'); if (mobileForgery) { label = mobileForgery.textContent.trim().toLowerCase(); firstTextNode = Array.from(mobileForgery.childNodes).find(node => node.nodeType === Node.TEXT_NODE); } // FORGERY if (forgeryPage) { let dropdownWrapper = subCrime.querySelector('.optionWithLevelRequirement___cHH35'); if ((!mobileForgery && subCrime.textContent === 'Begin a New Project') || dropdownWrapper) { forgery(subCrime, dataMap, maximumForgery, maximumForgeryTarget); } } // SEARCH FOR CASH (MOBILE) let mobileSearchForCash = subCrime.querySelector('.titleAndIcon___h8RJV'); if (mobileSearchForCash) { firstTextNode = Array.from(mobileSearchForCash.childNodes).find(node => node.nodeType === Node.TEXT_NODE); label = firstTextNode?.textContent.trim().toLowerCase(); } // GRAFFITI (MOBILE) let mobileGraffiti = subCrime.querySelector('.tabletTitleAndTagCount___vb0UQ'); if (mobileGraffiti) { firstTextNode = Array.from(mobileGraffiti.childNodes).find(node => node.nodeType === Node.TEXT_NODE); label = firstTextNode?.textContent.trim().toLowerCase(); } // SHOPLIFTING (MOBILE) let mobileShoplifting = subCrime.querySelector('.tabletShopTitle___aqRuE'); if (mobileShoplifting) label = mobileShoplifting.textContent.trim().toLowerCase(); // MOBILE MISC let mobileOther = document.body.querySelector('.area-mobile___BH0Ku'); // BACK TO THE MAIN CODE // let crimeData = dataMap.get(label); // SPECIAL CHANGES FOR SPECIFIC CRIMES // // CRACKING if (cracking) { label = crackingService.textContent.trim().toLowerCase(); crimeData = crackingDataMap.get(label); if (bfs === 6) { crimeData['$/N'] = crimeData['Estimated profit per nerve 6BFS']; } else if (bfs === 7) { crimeData['$/N'] = crimeData['7BFS']; } else { crimeData['$/N'] = crimeData['Estimated profit per nerve 6BFS']; } } // GRAFFITI (MOBILE) if (mobileGraffiti && !crimeData) crimeData = dataMap.get(`${label} district`); // If crime data does not exist (due to wrong element or other reason), skip the rest if (!crimeData) continue; // STYLE CHANGES subCrime.style.display = 'flex'; // MOBILE SEARCH FOR CASH AND SHOPLIFTING EXTRA STILE CHANGES if (!mobileSearchForCash && !mobileShoplifting) subCrime.style.justifyContent = 'space-between'; // MOBILE STYLE CHANGES let typeServiceWrapper = subCrime.querySelector('.typeAndServiceWrapper___AoONK'); if (typeServiceWrapper) typeServiceWrapper.style.flex = '1'; // BACK TO THE MAIN CODE // // CREATE VALUE SPAN let span = document.createElement('span'); span.classList.add('hf-value'); let moneyPerNerve = -Infinity; // FILL IN VALUE DATA IN SPAN if (crimeData['$/N']) { // Information found in the sheet moneyPerNerve = parseInt(crimeData['$/N']?.replace(/[$,]/g, '')); // Find the maximum money per nerve for this crime! if (moneyPerNerve > maximum) { maximum = moneyPerNerve maximumElement = subCrime.parentNode; } let spanText = `${moneyPerNerve < 0 ? '-' : ''}$${Math.abs(moneyPerNerve).toLocaleString('en-US')} / N`; span.textContent = spanText; // Color green by default, but red if negative $/N and yellow if below threshold but above 0 span.style.color = 'var(--default-base-green-color)'; if (moneyPerNerve < threshold) span.style.color = 'var(--default-base-gold-color)'; if (moneyPerNerve < 0) span.style.color = 'var(--default-base-important-color)'; // SPECIAL TOOLTIP FOR MOBILE GRAFFITI AND SHOPLIFTING if (mobileGraffiti) mobileGraffiti.title = spanText; if (mobileShoplifting) mobileShoplifting.title = spanText; } else { let spanText = '$/N N/A'; span.textContent = spanText; // Color yellow for N/A span.style.color = 'var(--default-base-gold-color)'; // SPECIAL TOOLTIP FOR MOBILE GRAFFITI AND SHOPLIFTING if (mobileGraffiti) mobileGraffiti.title = spanText; if (mobileShoplifting) mobileShoplifting.title = spanText; } // Set attribute to the item container, so I can easily use that later subCrime.parentNode.parentNode.parentNode.parentNode.setAttribute('data-hf-value', moneyPerNerve); // If the SPAN doesn't exist yet, append it! DIFFERENT FOR SPECIAL CRIMES let existingSpan = subCrime.querySelector('.hf-value'); if (!existingSpan) { if (cracking) { subCrime.appendChild(span); } else if (mobileSearchForCash) { mobileSearchForCash.insertBefore(span, mobileSearchForCash.childNodes[1] || null); } else if (!forgeryPage && !mobileGraffiti && !mobileShoplifting) { subCrime.insertBefore(span, subCrime.childNodes[1] || null); } } // SPECIAL STYLE CHANGES FOR SPECIFIC (MOBILE) CRIMES // let graffiti = subCrime.querySelector('.reputationIconWrapper___CM05s'); if (graffiti || pickpocketing) span.style.paddingLeft = '15px'; if (pickpocketing) span.style.flex = '2'; if (mobileOther && pickpocketing) span.style.fontSize = 'smaller' // On forgery, take care of the dropdown/overview container if (forgeryPage && !mobileForgery) { let div = document.createElement('div'); div.style.display = 'flex'; div.style.flexDirection = 'column'; let typeSpan = document.createElement('span'); typeSpan.textContent = firstTextNode.textContent; div.appendChild(typeSpan); div.appendChild(span); span.style.paddingTop = '5px'; subCrime.insertBefore(div, subCrime.childNodes[1] || null); firstTextNode.remove(); } else if (mobileForgery) { mobileForgery.appendChild(span); span.style.paddingLeft = '5px'; } subCrime.parentNode.style.background = ''; // So crimes like pickpocketing aren't always showing multiple options } // BACK TO THE MAIN CODE // if (maximumElement && maximum > threshold) maximumElement.style.background = 'var(--default-bg-green-hover-color)'; // DON'T SHOW SORT BUTTON ON PICKPOCKETING if (window.location.href.includes('pickpocketing')) return; // Button is just too hard with the way the crime works // CREATE A SORT BUTTON let existingButtons = document.body.querySelectorAll('.hf-sort-button'); if (existingButtons) { for (let button of existingButtons) { button.remove(); } } let button = createSortButton(); button.addEventListener('click', function () { let invalidOpposite = false; if (forgeryPage) invalidOpposite = true; // FORGERY has an overview as first "subcrime" sortButtonClick(list, button, invalidOpposite); }); } // Display value per nerve on BOOTLEGGING function bootlegging(data, retries = 30) { let optionWrappers = document.body.querySelectorAll('.crimeOptionWrapper___IOnLO'); // If the DOM hasn't loaded yet, try again! if (!optionWrappers || optionWrappers.length < 2) { if (retries > 0) { setTimeout(() => bootlegging(data, retries - 1), 100); } else { console.warn('[HF] Gave up looking for Bootlegging subtitles after 30 retries.'); } return; } // Map of UI labels to normalized data targets let labelToTargetMap = { 'sell counterfeit dvds': 'sell counterfeit dvds', 'online store': 'collect from online store', }; let maximum = -Infinity; let maximumElement = null; for (let optionWrapper of optionWrappers) { // Find the "Sell" and "Online" buttons if (!optionWrapper.textContent.toLowerCase().includes('sell') && !optionWrapper.textContent.toLowerCase().includes('online')) continue; let options = optionWrapper.querySelectorAll('.crimeOptionSection___hslpu'); for (let option of options) { let label = option.textContent.trim().toLowerCase(); let target = labelToTargetMap[label]; let moneyPerNerve = -Infinity; // If target can't be found, move on if (!target) continue; let match = data.find(c => c.target.toLowerCase() === target); if (match && match['$/N']) moneyPerNerve = parseInt(match['$/N'].replace('$', '')); // Create the span let span = document.createElement('span'); span.classList.add('hf-value'); span.textContent = `${moneyPerNerve < 0 ? '-' : ''}$${Math.abs(moneyPerNerve).toLocaleString('en-US')} / N`; span.style.paddingLeft = '15px'; span.style.color = 'var(--default-base-green-color)'; if (moneyPerNerve < threshold) span.style.color = 'var(--default-base-gold-color)'; if (moneyPerNerve < 0) span.style.color = 'var(--default-base-important-color)'; if (moneyPerNerve > maximum) { maximum = moneyPerNerve maximumElement = option.parentNode; } option.appendChild(span); } } if (maximum > threshold) maximumElement.style.background = 'var(--default-bg-green-hover-color)'; } // Display value per nerve on DISPOSAL function disposal(data, observed, retries = 30) { let mobile = document.body.querySelector('.area-mobile___BH0Ku'); let list = document.body.querySelector('.virtualList___noLef'); let subCrimes = list?.querySelectorAll('.crimeOptionSection___hslpu'); // If the DOM hasn't fully loaded yet, try again if (!list || !subCrimes || subCrimes.length < 2) { if (retries > 0) { setTimeout(() => disposal(data, observed, retries - 1), 100); } else { console.warn('[HF] Gave up looking for Disposal subtitles after 30 retries.'); } return; } if (!observed) { createObserver(list, data, crackingData, true); } let disposalData = {}; for (let crimeData of data) { // If crime isn't disposal, no need to loop through it if (crimeData.crime !== 'Disposal') continue; // Disposal is formatted in the sheet as "Subtitle: Type" let [targetTitle, targetType] = crimeData.target.split(':').map(s => s.trim()); if (!disposalData[targetTitle.trim().toLowerCase()]) { disposalData[targetTitle.trim().toLowerCase()] = {}; } disposalData[targetTitle.trim().toLowerCase()][targetType] = parseInt(crimeData['$/N']?.replace(/[$,]/g, '')); } let maximum = -Infinity; let maximumElement = null; for (let subCrime of subCrimes) { let firstTextNode = Array.from(subCrime.childNodes).find(node => node.nodeType === Node.TEXT_NODE); let label = firstTextNode?.textContent.trim().toLowerCase(); let data = disposalData[label] if (!data) continue; // If data is not found, break it off here let maximumValue = -Infinity; let maximumType = null; for (let type in data) { if (data[type] > maximumValue) { maximumValue = data[type]; maximumType = type; } } // Take care of the method buttons let methods = subCrime.parentNode.querySelectorAll('.methodButton___lCgpf'); for (let method of methods) { let ariaLabel = method.getAttribute('aria-label'); let info = disposalData[label][ariaLabel.trim()]; // Display $/N upon hover method.title = `${info < 0 ? '-' : ''}$${Math.abs(info).toLocaleString('en-US')} / N`; // Give the method borders based on profitability if (info < 0) method.style.border = '1px solid var(--default-base-important-color)'; if (info >= 0) method.style.border = '1px solid var(--default-base-gold-color)'; if (ariaLabel.trim().toLowerCase() === maximumType.toLowerCase()) method.style.border = '1px solid var(--default-base-green-color)'; } if (maximumValue > maximum) { maximum = maximumValue maximumElement = subCrime.parentNode.parentNode; } // Create value div let div = document.createElement('div'); div.classList.add('hf-value'); div.style.display = 'flex'; div.style.flexDirection = 'column'; div.style.alignItems = 'flex-end'; div.style.color = 'var(--default-base-green-color)'; if (maximumValue < threshold) div.style.color = 'var(--default-base-gold-color)'; if (maximumValue < 0) div.style.color = 'var(--default-base-important-color)'; let type = document.createElement('span'); type.textContent = maximumType; div.appendChild(type); let value = document.createElement('span'); value.textContent = `${maximumValue < 0 ? '-' : ''}$${Math.abs(maximumValue).toLocaleString('en-US')} / N`; div.appendChild(value); let existingDiv = subCrime.querySelector('.hf-value'); if (!existingDiv) subCrime.appendChild(div); subCrime.style.display = 'flex'; subCrime.style.justifyContent = 'space-between'; // MOBILE STYLE CHANGES if (mobile) { subCrime.style.flexWrap = 'wrap'; subCrime.style.alignContent = 'center'; div.style.paddingTop = '5px'; div.style.flexDirection = ''; type.textContent += ':'; value.style.paddingLeft = '5px'; } subCrime.parentNode.parentNode.parentNode.parentNode.setAttribute('data-hf-value', maximumValue); subCrime.parentNode.parentNode.style.background = ''; // So it isn't showing multiple options upon page refresh } if (maximumElement && maximum > threshold) maximumElement.style.background = 'var(--default-bg-green-hover-color)'; let existingButtons = document.body.querySelectorAll('.hf-sort-button'); if (existingButtons) { for (let button of existingButtons) { button.remove(); } } let button = createSortButton(); button.addEventListener('click', function () { sortButtonClick(list, button); }); } // HELPER FUNCTION for the dropdown and "Begin a New Project" in FORGERY function forgery(subCrime, dataMap, maximum, maximumTarget) { let mobileForgery = document.body.querySelector('.tabletProjectTitle___Wsdf7'); let mobile = document.body.querySelector('.area-mobile___BH0Ku'); if (subCrime.textContent === 'Begin a New Project') { if (mobile) return; // No "Begin a New Project" title on mobile // Don't just keep adding info! let existingDiv = document.body.querySelector('hf-best-forgery'); if (existingDiv) return; let div = document.createElement('div'); div.classList.add('hf-best-forgery'); div.style.display = 'flex'; div.style.alignItems = 'flex-end'; div.style.color = 'var(--default-base-green-color)'; if (maximum < threshold) div.style.color = 'var(--default-base-gold-color)'; if (maximum < 0) div.style.color = 'var(--default-base-important-color)'; let maximumTargetEl = document.createElement('span'); maximumTargetEl.textContent = `${maximumTarget}`; let maximumEl = document.createElement('span'); maximumEl.textContent = `(${maximum < 0 ? '-' : ''}$${Math.abs(maximum).toLocaleString('en-US')} / N)`; maximumEl.style.paddingLeft = '5px'; div.appendChild(maximumTargetEl); div.appendChild(maximumEl); subCrime.style.display = 'flex'; subCrime.style.justifyContent = 'space-between'; subCrime.appendChild(div); return; } let dropdownWrapper = subCrime.querySelector('.optionWithLevelRequirement___cHH35'); if (dropdownWrapper) { let typesEl = dropdownWrapper.lastChild; // Don't just keep adding info! let existingMainSpan = document.body.querySelector('.hf-selected-dropdown-value'); if (existingMainSpan) return; let mainSpan = document.createElement('span'); mainSpan.classList.add('hf-selected-dropdown-value'); mainSpan.textContent = ` (${maximum < 0 ? '-' : ''}$${Math.abs(maximum).toLocaleString('en-US')} / N)`; mainSpan.style.display = 'contents'; mainSpan.style.color = 'var(--default-base-green-color)'; if (mainSpan < threshold) mainSpan.style.color = 'var(--default-base-gold-color)'; if (maximum < 0) mainSpan.style.color = 'var(--default-base-important-color)'; dropdownWrapper.appendChild(mainSpan); let ul = subCrime.querySelector('ul'); let lists = ul.querySelectorAll('li'); for (let list of lists) { // Don't just keep adding info! let existingSpan = list.querySelector('.hf-dropdown-value'); if (existingSpan) continue; let id = list.id; id = id.replace(/-/g, ' ').replace(/\d+$/, '').replace('option', ''); let crimeData = dataMap.get(id.trim().toLowerCase()); if (!crimeData['$/N']) continue; let option = list.querySelector('.optionWithLevelRequirement___cHH35'); let listExplanation = option.lastChild; let value = parseInt(crimeData['$/N']?.replace(/[$,]/g, '')); // Create a green span for the value let span = document.createElement('span'); span.classList.add('hf-dropdown-value'); span.style.display = 'contents'; span.style.color = 'var(--default-base-green-color)'; if (value < threshold) span.style.color = 'var(--default-base-gold-color)'; if (value < 0) span.style.color = 'var(--default-base-important-color)'; let amount = parseInt(crimeData['$/N']?.replace(/[$,]/g, '')); span.textContent = ` (${amount < 0 ? '-' : ''}$${Math.abs(amount).toLocaleString('en-US')})`; option.appendChild(span); continue; } } } // HELPER FUNCTION to create the sort button function createSortButton() { // Remove any leftover buttons! let existingButtons = document.body.querySelectorAll('.hf-sort-button'); if (existingButtons) { for (let button of existingButtons) { button.remove(); } } let headerElement = document.body.querySelector('.crimes-app-header'); let button = document.createElement('button'); button.textContent = '▲ Profitability'; button.classList.add('hf-sort-button'); button.style.background = 'var(--input-money-error-border-color)'; button.style.color = 'var(--btn-color)'; button.style.borderRadius = '8px'; button.style.cursor = 'pointer'; button.style.fontWeight = 'bold'; headerElement.insertBefore(button, headerElement.lastElementChild); return button; } // HELPER FUNCTION to call when the sort button is clicked function sortButtonClick(list, button, invalidOpposite) { let subCrimeArray = Array.from(list.querySelectorAll('.virtual-item')); subCrimeArray.sort((a, b) => { let aRaw = a.getAttribute('data-hf-value'); let bRaw = b.getAttribute('data-hf-value'); let aVal = parseFloat(aRaw); let bVal = parseFloat(bRaw); let aInvalid = isNaN(aVal); let bInvalid = isNaN(bVal); // Push invalid (missing or NaN) values to the bottom or top depending on the crime if (invalidOpposite && (!a.classList.contains('virtualItemsBackdrop___oTwUm') || !b.classList.contains('virtualItemsBackdrop___oTwUm'))) { if (aInvalid && !bInvalid) return -1; if (!aInvalid && bInvalid) return 1; if (aInvalid && bInvalid) return 0; } else { if (aInvalid && !bInvalid) return 1; if (!aInvalid && bInvalid) return -1; if (aInvalid && bInvalid) return 0; } if (button.textContent.includes('▲')) { return bVal - aVal; // highest first } else { return aVal - bVal; // lowest first } }); // Reposition elements visually subCrimeArray.forEach((el, i) => { if (invalidOpposite) { if (i === 0) { return; } else if (i === 1) { el.style.transform = `translateY(64px)`; return; } el.style.transform = `translateY(${((i - 1) * 51) + 64}px)`; } else { el.style.transform = `translateY(${i * 51}px)`; } }); let backDrop = list.querySelector('.virtualItemsBackdrop___oTwUm'); // Toggle sort direction button.textContent = button.textContent.includes('▲') ? '▼ Profitability' : '▲ Profitability'; } // BURGLARY create sheets button function createSheetsButton() { // Don't just keep adding buttons! let existingButton = document.body.querySelector('.hf-sheets-button'); if (existingButton) return; let headerElement = document.body.querySelector('.crimes-app-header'); let button = document.createElement('button'); button.textContent = 'Google Sheets'; button.title = `Crime Profitability will be added soonTM for this crime (data pending)`; button.classList.add('hf-sheets-button'); button.style.background = 'var(--input-money-error-border-color)'; button.style.color = 'var(--btn-color)'; button.style.borderRadius = '8px'; button.style.cursor = 'pointer'; button.style.fontWeight = 'bold'; headerElement.insertBefore(button, headerElement.lastElementChild); button.addEventListener('click', function () { window.open( 'https://docs.google.com/spreadsheets/d/13wUFhhssuPdAONI_OmRJi6l_Bs7KRZXDgVFCn7uJJNQ/edit?gid=706159343#gid=706159343', '_blank' ); }); return button; } // UNAVAILABLE CRIME create sheets button function createUnavailable() { // Don't just keep adding buttons! let existingButton = document.body.querySelector('.hf-sheets-button'); if (existingButton) return; let headerElement = document.body.querySelector('.crimes-app-header'); let button = document.createElement('button'); button.textContent = 'Google Sheets'; button.title = `Crime Profitability is (currently) unavailable for this crime`; button.classList.add('hf-sheets-button'); button.style.background = 'var(--input-money-error-border-color)'; button.style.color = 'var(--btn-color)'; button.style.borderRadius = '8px'; button.style.cursor = 'pointer'; button.style.fontWeight = 'bold'; headerElement.insertBefore(button, headerElement.lastElementChild); button.addEventListener('click', function () { window.open( 'https://docs.google.com/spreadsheets/d/13wUFhhssuPdAONI_OmRJi6l_Bs7KRZXDgVFCn7uJJNQ/edit?gid=0#gid=0', '_blank' ); }); return button; } // CRIME HUB display value per nerve function crimeHub(data, retries = 30) { let crimeHubRoot = document.body.querySelector('.crimes-hub-root'); let crimes = crimeHubRoot?.querySelectorAll('.crimes-hub-crime'); if (!crimeHubRoot || !crimes || crimes.length < 2) { if (retries > 0) { setTimeout(() => crimeHub(data, retries - 1), 100); } else { console.warn('[HF] Gave up looking for the crime hub after 30 retries.'); } return; } for (let crime of crimes) { let titleElement = crime.querySelector('.crimeTitle___Q9cpR'); let title = titleElement?.textContent.trim(); if (title) { findMaximumProfit(data, title, crime); } } } // HELPER FUNCTION for the CRIME HUB to find and display maximum profit per crime type function findMaximumProfit(data, crimeTitle, crimeElement) { if (!settings[crimeTitle]) return; // Don't just keep adding stuff! let alreadyAdded = crimeElement.querySelector('.hf-value'); if (alreadyAdded) return; let maximum = -Infinity; for (let crimeData of data) { if (crimeData.crime.toLowerCase() !== crimeTitle.toLowerCase()) continue; let moneyPerNerve = parseInt(crimeData['$/N']?.replace(/[$,]/g, '')); if (moneyPerNerve > maximum) maximum = moneyPerNerve; } let span = document.createElement('span'); span.textContent = `${maximum < 0 ? '-' : ''}$${Math.abs(maximum).toLocaleString('en-US')} / N`; span.classList.add('hf-value'); span.style.position = 'absolute'; span.style.justifySelf = 'anchor-center'; span.style.marginTop = '-38px'; span.style.padding = '8px'; span.style.borderRadius = '15px'; span.style.background = 'var(--default-bg-19-gradient)'; span.style.color = 'var(--default-base-white-color)'; if (maximum < threshold) span.style.background = 'var(--default-bg-18-gradient)'; if (maximum < 0) span.style.background = 'var(--default-bg-17-gradient)'; if (maximum === -Infinity) { span.textContent = '$/N N/A'; span.style.background = 'var(--default-bg-18-gradient)'; } let titleBar = crimeElement.querySelector('.titleBar___O6ygy'); if (!titleBar) { titleBar = crimeElement.querySelector('.titleAndStatus___q2yBJ'); let statusGroups = titleBar.querySelector('.statusGroups___EtLVR'); titleBar.insertBefore(span, statusGroups); span.style.position = ''; span.style.justifySelf = ''; span.style.marginTop = ''; span.style.background = ''; span.style.color = 'var(--default-base-green-color)'; span.style.padding = ''; span.style.borderRadius = ''; span.style.marginBottom = '-3px'; if (maximum < threshold) span.style.color = 'var(--default-base-gold-color)'; if (maximum < 0) span.style.color = 'var(--default-base-important-color)'; if (maximum === -Infinity) span.style.color = 'var(--default-base-gold-color)'; } else { titleBar.insertBefore(span, titleBar.firstChild); } } // HELPER FUNCTION to convert a CSV string into an array of objects function parseCSV(text) { let rows = text.trim().split('\n'); let parseRow = (row) => { let regex = /"(.*?)"(?:,|$)/g; let result = []; let match; while ((match = regex.exec(row)) !== null) result.push(match[1]); return result.slice(0, 8); // Only keep first 8!! columns }; let headers = parseRow(rows.shift()); // Rename the first "7BFS" to "7BFS attempts" let found = false; headers = headers.map(header => { if (header === '7BFS' && !found) { found = true; return '7BFS attempts'; } return header; }); return rows.map(row => { let values = parseRow(row); let entry = {}; headers.forEach((header, index) => { entry[header] = values[index]; }); return entry; }); } // FETCH FUNCTION to fetch data from Emforus' sheet function fetchData() { let currentEpoch = Math.floor(Date.now() / 1000); let twentyFourHours = 86400; if (rememberedCrackingData && lastFetched && (currentEpoch - lastFetched) <= twentyFourHours) { console.log('[HF] Using cached Crime Profitability data.'); let data = rememberedData; cachedData = data; let crackingData = rememberedCrackingData; cachedCrackingData = crackingData; displayData(data, crackingData); return; } let urls = { all: emforusData, cracking: crackingData }; let results = {}; let completed = 0; let total = Object.keys(urls).length; Object.entries(urls).forEach(([type, url]) => { GM[httpRequest]({ method: 'GET', url: url, responseType: 'text', onload: function(response) { const data = parseCSV(response.responseText); results[type] = data; if (type === 'all') { cachedData = data; localStorage.setItem('hf-crime-profitability-data', JSON.stringify(data)); } else if (type === 'cracking') { cachedCrackingData = data; localStorage.setItem('hf-crime-profitability-cracking-data', JSON.stringify(data)); } completed++; if (completed === total) { let currentEpoch = Math.floor(Date.now() / 1000); localStorage.setItem('hf-crime-profitability-last-fetched', currentEpoch); displayData(results.all, results.cracking); } }, onerror: function(response) { console.error(`Error fetching ${type} data:`, response); } }); }); } // HELPER FUNCTION to create a mutation observer to watch for changes on the page function createObserver(element, info, crackingInfo, disposalPage) { let target; target = element; if (!target) { console.error(`[HF] Mutation Observer target not found.`); return; } let observer = new MutationObserver(function(mutationsList, observer) { for (let mutation of mutationsList) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.classList.contains('virtual-item')) { if (!disposalPage) { crimePage(info, crackingInfo, true); } else { disposal(info, true); } } }); mutation.removedNodes.forEach(node => { if (node.classList.contains('virtual-item')) { if (!disposalPage) { crimePage(info, crackingInfo, true); } else { disposal(info, true); } } }); } } }); let config = { attributes: true, childList: true, subtree: true, characterData: true }; observer.observe(target, config); } // Attach click event listener document.body.addEventListener('click', handleButtonClick); function handleButtonClick(event) { setTimeout(() => { handlePageChange(); }, 500); } // Check if there's a page chance - if yes, rerun script function handlePageChange() { if (window.location.href === currentHref) return; let existingButton = document.body.querySelector('.hf-sort-button'); if (existingButton) existingButton.remove(); displayData(cachedData, cachedCrackingData); currentHref = window.location.href; } // Adds a settings button to the CRIME HUB page function createSettingsButton() { // Don't just keep adding buttons! let existingButton = document.body.querySelector('.hf-sort-button'); if (existingButton) return; let mobile = document.body.querySelector('.area-mobile___BH0Ku'); let headerElement = document.body.querySelector('.crimes-app-header'); if (!headerElement) headerElement = document.body.querySelector('.content-title'); let button = document.createElement('button'); button.textContent = 'Crime Profitability Settings'; button.classList.add('hf-sort-button'); button.style.background = 'var(--input-money-error-border-color)'; button.style.color = 'var(--btn-color)'; button.style.borderRadius = '8px'; button.style.cursor = 'pointer'; button.style.fontWeight = 'bold'; if (mobile) { button.textContent = `$/N Settings`; headerElement.style.flexWrap = 'wrap'; headerElement.style.height = 'fit-content'; headerElement.style.justifyContent = 'flex-end'; headerElement.style.paddingBottom = '10px'; headerElement.style.rowGap = '10px'; headerElement.style.columnGap = '20px'; } headerElement.insertBefore(button, headerElement.children[1]); button.addEventListener('click', function () { createModal(); }); return button; } // Function to create the SETTINGS modal function createModal() { let mobile = document.body.querySelector('.area-mobile___BH0Ku'); let cachedSettings = settings; let cachedThreshold = threshold; let modal = document.createElement('div'); modal.style.position = 'fixed'; modal.style.top = '50%'; modal.style.left = '50%'; modal.style.transform = 'translate(-50%, -50%)'; modal.style.padding = '20px'; modal.style.backgroundColor = 'var(--sidebar-area-bg-warning-active)'; modal.style.border = '2px solid var(--default-tabs-color)'; modal.style.borderRadius = '15px'; modal.style.maxWidth = '400px'; modal.style.zIndex = '9999'; modal.style.maxHeight = '60vh'; modal.style.overflow = 'hidden'; modal.style.display = 'flex'; modal.style.flexDirection = 'column'; let titleContainer = document.createElement('div'); titleContainer.textContent = 'Crime Profitability Settings'; titleContainer.style.fontSize = 'x-large'; titleContainer.style.fontWeight = 'bolder'; titleContainer.style.paddingBottom = '8px'; let scrollContainer = document.createElement('div'); scrollContainer.style.maxHeight = '100%'; scrollContainer.style.flex = '1'; // Fill remaining space scrollContainer.style.overflowY = 'auto'; scrollContainer.style.marginTop = '10px'; let mainContainer = document.createElement('div'); mainContainer.style.display = 'flex'; mainContainer.style.flexDirection = 'column'; scrollContainer.appendChild(mainContainer); let creditSpan = document.createElement('span'); creditSpan.innerHTML = `Powered by <a href="https://www.torn.com/profiles.php?XID=2626587" target="_blank" rel="noopener noreferrer" style="color: var(--default-blue-color);">Heartflower [2626587]</a> and the <a href="https://docs.google.com/spreadsheets/d/13wUFhhssuPdAONI_OmRJi6l_Bs7KRZXDgVFCn7uJJNQ/edit?gid=560321570#gid=560321570" target="_blank" rel="noopener noreferrer" style="color: var(--default-blue-color);">Crime Profitability Index Google Sheets</a> by <a href="https://www.torn.com/profiles.php?XID=2535044" target="_blank" rel="noopener noreferrer" style="color: var(--default-blue-color);">Emforus [2535044]</a>.`; creditSpan.style.paddingBottom = '8px'; mainContainer.appendChild(creditSpan); let checkboxDiv = document.createElement('div'); checkboxDiv.style.display = 'flex'; checkboxDiv.style.flexDirection = 'column'; checkboxDiv.style.paddingTop = '15px'; let infoSpan = document.createElement('span'); infoSpan.textContent = 'Enable/disable which crimes you want crime profitability to appear for here.'; checkboxDiv.appendChild(infoSpan); if (!mobile) { checkboxDiv.style.flexDirection = 'row'; checkboxDiv.style.flexWrap = 'wrap'; infoSpan.style.flex = '1 1 100%'; let leftColumn = document.createElement('div'); leftColumn.style.display = 'flex'; leftColumn.style.flexDirection = 'column'; let middleColumn = document.createElement('div'); middleColumn.style.display = 'flex'; middleColumn.style.flexDirection = 'column'; middleColumn.style.paddingLeft = '30px'; let rightColumn = document.createElement('div'); rightColumn.style.display = 'flex'; rightColumn.style.flexDirection = 'column'; rightColumn.style.paddingLeft = '30px'; let crimes = ['Overview', 'Search for Cash', 'Bootlegging', 'Graffiti', 'Shoplifting', 'Pickpocketing', 'Card Skimming', 'Burglary', 'Hustling', 'Disposal', 'Cracking', 'Forgery', 'Scamming']; let enableAll = addToggle(checkboxDiv, 'Enable All'); enableAll.style.flex = '1 1 100%'; checkboxDiv.appendChild(leftColumn); checkboxDiv.appendChild(middleColumn); checkboxDiv.appendChild(rightColumn); crimes.forEach((crime, index) => { const parent = [leftColumn, middleColumn, rightColumn][index % 3]; addToggle(parent, crime); }); } else { modal.style.minWidth = '60vw'; titleContainer.style.fontSize = 'large'; titleContainer.textContent = '$/N Settings'; let crimes = ['Enable All', 'Overview', 'Search for Cash', 'Bootlegging', 'Graffiti', 'Shoplifting', 'Pickpocketing', 'Card Skimming', 'Burglary', 'Hustling', 'Disposal', 'Cracking', 'Forgery', 'Scamming']; for (let crime of crimes) { addToggle(checkboxDiv, crime); } } createToggleStyleSheet(); let warningSpan = document.createElement('span'); warningSpan.textContent = `* Currently (temporarily) unavailable`; warningSpan.style.paddingTop = '5px'; checkboxDiv.appendChild(warningSpan); mainContainer.appendChild(checkboxDiv); let inputContainer = document.createElement('div'); inputContainer.style.paddingTop = '20px'; inputContainer.style.display = 'flex'; inputContainer.style.flexDirection = 'column'; let inputTitle = document.createElement('span'); inputTitle.textContent = '$/N Threshold'; inputTitle.style.fontWeight = 'bold'; inputTitle.style.fontSize = 'larger'; inputTitle.style.paddingBottom = '5px'; inputContainer.appendChild(inputTitle); let inputExplanation = document.createElement('span'); inputExplanation.textContent = `You're able to set a preferred minimum value per nerve in the following input. If your threshold is set to $5,000, anything between $0 and $5,000 will appear in a yellow value and will no longer be highlighted in green as "highest value". If no threshold is filled in, it will be $0 by default.`; inputExplanation.style.paddingBottom = '8px'; inputContainer.appendChild(inputExplanation); let inputLabelContainer = document.createElement('div'); inputContainer.appendChild(inputLabelContainer); let inputLabel = document.createElement('span'); inputLabel.textContent = 'Minimum $/N threshold:'; inputLabel.style.paddingRight = '5px'; inputLabelContainer.appendChild(inputLabel); createTextNumberInput(inputLabelContainer); mainContainer.appendChild(inputContainer); // Create a container for "Done" and "Cancel" buttons let buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.justifyContent = 'space-between'; buttonContainer.style.paddingTop = '15px'; let cancelButton = document.createElement('button'); cancelButton.textContent = 'Cancel'; cancelButton.style.color = 'black'; cancelButton.style.border = '1px solid black'; cancelButton.style.borderRadius = '5px'; cancelButton.style.backgroundColor = '#ccc'; cancelButton.addEventListener('click', function () { settings = cachedSettings; threshold = cachedThreshold; localStorage.setItem('hf-crime-profitability-settings', JSON.stringify(settings)); localStorage.setItem('hf-crime-profitability-threshold', threshold); modal.style.display = 'none'; }); buttonContainer.appendChild(cancelButton); // Add style for hover effect cancelButton.style.cursor = 'pointer'; // Add event listeners for hover effect cancelButton.addEventListener('mouseover', function () { cancelButton.style.fontWeight = 'bold'; }); cancelButton.addEventListener('mouseout', function () { cancelButton.style.fontWeight = ''; }); let doneButton = document.createElement('button'); doneButton.textContent = 'Done'; doneButton.style.color = 'black'; doneButton.style.border = '1px solid black'; doneButton.style.borderRadius = '5px'; doneButton.style.backgroundColor = '#ccc'; doneButton.addEventListener('click', function () { location.reload(); modal.style.display = 'none'; }); buttonContainer.appendChild(doneButton); // Add style for hover effect doneButton.style.cursor = 'pointer'; // Add event listeners for hover effect doneButton.addEventListener('mouseover', function () { doneButton.style.fontWeight = 'bold'; }); doneButton.addEventListener('mouseout', function () { doneButton.style.fontWeight = ''; }); modal.appendChild(titleContainer); modal.appendChild(scrollContainer); modal.appendChild(buttonContainer); document.body.appendChild(modal); return modal; } // Add the checkbox toggle in the SETTINGS modal function addToggle(container, labelText) { let toggleContainer = document.createElement('div'); toggleContainer.classList.add('hf-toggle-container'); toggleContainer.style.paddingTop = '5px'; let text = document.createElement('span'); text.textContent = labelText; text.style.paddingLeft = '5px'; let label = document.createElement('label'); label.classList.add('switch'); let input = document.createElement('input'); input.type = 'checkbox'; input.classList.add('hf-checkbox'); input.checked = true; // Check on by default let span = document.createElement('span'); span.classList.add('slider', 'round'); if (labelText === 'Graffiti' || labelText === 'Card Skimming' || labelText === 'Burglary' || labelText === 'Hustling' || labelText === 'Scamming') { text.textContent += '*'; input.checked = false; } let savedInfo = settings[labelText]; if (savedInfo === true) input.checked = true; if (savedInfo === false) input.checked = false; if (labelText === 'Enable All') input.checked = false; settings[labelText] = input.checked; localStorage.setItem('hf-crime-profitability-settings', JSON.stringify(settings)); toggleContainer.appendChild(label); toggleContainer.appendChild(text); label.appendChild(input); label.appendChild(span); container.appendChild(toggleContainer); // Add event listener to detect changes in the checkbox state input.addEventListener('change', function() { if (input.checked) { if (labelText === 'Enable All') { let inputs = document.body.querySelectorAll('.hf-checkbox'); for (let input of inputs) { input.checked = true; let event = new Event('change', { bubbles: true }); input.dispatchEvent(event); } text.textContent = 'Disable All'; return; } settings[labelText] = true; localStorage.setItem('hf-crime-profitability-settings', JSON.stringify(settings)); } else { if (labelText === 'Enable All' || labelText === 'Disable All') { let inputs = document.body.querySelectorAll('.hf-checkbox'); for (let input of inputs) { input.checked = false; let event = new Event('change', { bubbles: true }); input.dispatchEvent(event); } text.textContent = 'Enable All'; return; } settings[labelText] = false; localStorage.setItem('hf-crime-profitability-settings', JSON.stringify(settings)); } }); return toggleContainer; } // HELPER FUNCTION to create a "Number" input function createTextNumberInput(element) { let input = document.createElement('input'); input.className = 'hf-number-input'; input.type = 'text'; // Allow '5K', '1.2M', etc. input.value = threshold.toLocaleString('en-US') || ''; input.style.padding = '5px'; input.style.borderRadius = '5px'; input.style.border = '1px solid black'; input.style.background = '#ccc'; input.style.width = '70px'; element.appendChild(input); input.addEventListener('keydown', function (e) { let allowedKeys = [ 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', '.', 'k', 'K', 'm', 'M', 'b', 'B', ',' ]; let isDigit = e.key >= '0' && e.key <= '9'; if (!isDigit && !allowedKeys.includes(e.key)) { e.preventDefault(); } }); input.addEventListener('input', function () { let rawValue = input.value.trim().toUpperCase(); let parsedValue = parseShorthandNumber(rawValue); if (!isNaN(parsedValue)) { input.value = parsedValue.toLocaleString('en-US'); threshold = parsedValue; localStorage.setItem('hf-crime-profitability-threshold', parsedValue); } }); } // HELPER FUNCTION to parse shorthand numbers function parseShorthandNumber(str) { str = str.replace(/,/g, ''); // Remove commas if (/^\d+(\.\d+)?$/.test(str)) { return parseFloat(str); } let match = str.match(/^(\d+(\.\d+)?)([KMB])$/); if (!match) return NaN; let number = parseFloat(match[1]); let suffix = match[3]; switch (suffix) { case 'K': return number * 1_000; case 'M': return number * 1_000_000; case 'B': return number * 1_000_000_000; default: return NaN; } } // HELPER FUNCTION to create a style sheet to make the fancier toggles work function createToggleStyleSheet() { let styles = ` .switch { position: relative; display: inline-block; width: 20px; height: 10px; top: 1px; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; } .slider:before { position: absolute; content: ""; height: 10px; width: 10px; background-color: white; transition: .4s; } input:checked + .slider { background-color: #2196F3; } input:focus + .slider { box-shadow: 0 0 1px #2196F3; } input:checked + .slider:before { transform: translateX(10px); } .slider.round { border-radius: 34px; } .slider.round:before { border-radius: 50%; } `; // Add the styles to a <style> tag in the document head let styleSheet = document.createElement("style"); styleSheet.type = "text/css"; styleSheet.innerText = styles; document.head.appendChild(styleSheet); } // MAIN FUNCTION TO DISPLAY EVERYTHING BASED ON HREF function displayData(data, crackingData) { let href = window.location.href; let currentHref = href; let crimes = ['Overview', 'Search for Cash', 'Bootlegging', 'Graffiti', 'Shoplifting', 'Pickpocketing', 'Card Skimming', 'Burglary', 'Hustling', 'Disposal', 'Cracking', 'Forgery', 'Scamming']; if (!settings || Object.keys(settings).length === 0) { settings = {}; for (let crime of crimes) { settings[crime] = true; if (crime === 'Graffiti' || crime === 'Card Skimming' || crime === 'Burglary' || crime === 'Hustling' || crime === 'Scamming') settings[crime] = false; } } if (href.includes('searchforcash')) { if (settings['Search for Cash']) crimePage(data); } else if (href.includes('bootlegging')) { if (settings.Bootlegging) bootlegging(data); } else if (href.includes('graffiti')) { if (settings.Graffiti) createUnavailable(); //crimePage(data); } else if (href.includes('shoplifting')) { if (settings.Shoplifting) crimePage(data); } else if (href.includes('pickpocketing')) { if (settings.Pickpocketing) crimePage(data); } else if (href.includes ('cardskimming')) { // Cannot be calculated as per Emforus if (settings['Card Skimming']) createUnavailable(); } else if (href.includes('burglary')) { // On my to do list for if the sheet gets updated if (settings.Burglary) createSheetsButton(); // Creates a button that links to the google sheets for the meantime } else if (href.includes('hustling')) { // Cannot be calculated as per Emforus if (settings.Hustling) createUnavailable(); } else if (href.includes('disposal')) { if (settings.Disposal) disposal(data); } else if (href.includes('cracking')) { if (settings.Cracking) crimePage(data, crackingData); } else if (href.includes('forgery')) { if (settings.Forgery) crimePage(data); } else if (href.includes('scamming')) { // Cannot be calculated as per Emforus if (settings.Scamming) createUnavailable(); } else { setTimeout(() => createSettingsButton(), 500); if (settings.Overview) crimeHub(data); } } fetchData(); // Checking arrows and document click handler only work like half of the time, so interval setInterval(handlePageChange, 200); })();