// ==UserScript==
// @name Crime Profitability
// @namespace heartflower.torn
// @version 1.1
// @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';
// Cached cracking data!
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;
// 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
span.style.color = 'var(--default-base-green-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) 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
if (observed && crackingData) {
let existingButton = document.body.querySelector('.hf-sort-button');
if (existingButton) existingButton.remove();
}
// CREATE A SORT BUTTON
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 < 0) span.style.color = 'var(--default-base-important-color)';
if (moneyPerNerve > maximum) {
maximum = moneyPerNerve
maximumElement = option.parentNode;
}
option.appendChild(span);
}
}
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 < 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) maximumElement.style.background = 'var(--default-bg-green-hover-color)';
if (observed) {
let existingButton = document.body.querySelector('.hf-sort-button');
if (existingButton) existingButton.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');
if (subCrime.textContent === 'Begin a New Project') {
if (mobileForgery) return; // No "Begin a New Project" title on mobile
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 < 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;
let mainSpan = document.createElement('span');
mainSpan.textContent = ` (${maximum < 0 ? '-' : ''}$${Math.abs(maximum).toLocaleString('en-US')} / N)`;
mainSpan.style.display = 'contents';
mainSpan.style.color = 'var(--default-base-green-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) {
let id = list.id;
id = id.replace(/-/g, ' ').replace(/\d+$/, '').replace('option', '');
let crimeData = dataMap.get(id.trim().toLowerCase());
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.style.display = 'contents';
span.style.color = 'var(--default-base-green-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() {
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() {
let headerElement = document.body.querySelector('.crimes-app-header');
let button = document.createElement('button');
button.textContent = 'Crime Profitability Sheet';
button.title = `Crime Profitability will be added soonTM for this crime (data pending)`;
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);
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() {
let headerElement = document.body.querySelector('.crimes-app-header');
let button = document.createElement('button');
button.textContent = 'Crime Profitability Sheet';
button.title = `Crime Profitability is (currently) unavailable for this crime`;
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);
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;
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 < 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 < 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;
console.log('Data', data);
console.log('Cached data', cachedData);
console.log('Cracking data', crackingData);
console.log('Cached cracking data', cachedCrackingData);
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() {
let mobile = document.body.querySelector('.area-mobile___BH0Ku');
let headerElement = document.body.querySelector('.crimes-app-header');
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.insertBefore(button, headerElement.children[1]);
button.addEventListener('click', function () {
createModal();
});
return button;
}
// Function to create the SETTINGS modal
function createModal() {
let cachedSettings = settings;
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.zIndex = '9999';
let titleContainer = document.createElement('div');
titleContainer.textContent = 'Crime Profitability Settings';
titleContainer.style.fontSize = 'medium';
titleContainer.style.fontWeight = 'bolder';
titleContainer.style.paddingBottom = '8px';
modal.appendChild(titleContainer);
let toggleContainer = document.createElement('div');
toggleContainer.style.display = 'flex';
toggleContainer.style.flexDirection = 'column';
let checkboxDiv = document.createElement('div');
checkboxDiv.style.display = 'flex';
checkboxDiv.style.flexDirection = 'column';
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';
checkboxDiv.appendChild(creditSpan);
let infoSpan = document.createElement('span');
infoSpan.textContent = 'Enable/disable which crimes you want crime profitability to appear for here.';
checkboxDiv.appendChild(infoSpan);
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) {
let checkbox = addToggle(checkboxDiv, crime);
}
createToggleStyleSheet();
let warningSpan = document.createElement('span');
warningSpan.textContent = `* Currently (temporarily) unavailable`;
warningSpan.style.paddingTop = '5px';
checkboxDiv.appendChild(warningSpan);
toggleContainer.appendChild(checkboxDiv);
// 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';
toggleContainer.appendChild(buttonContainer);
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;
localStorage.setItem('hf-crime-profitability-settings', JSON.stringify(settings));
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(toggleContainer);
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));
}
});
}
// 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);
})();