Crime Profitability

Show value per nerve on the crime pages

  1. // ==UserScript==
  2. // @name Crime Profitability
  3. // @namespace heartflower.torn
  4. // @version 1.3
  5. // @description Show value per nerve on the crime pages
  6. // @author Heartflower [2626587]
  7. // @match https://www.torn.com/loader.php?sid=crimes*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
  9. // @grant GM.xmlHttpRequest
  10. // ==/UserScript==
  11.  
  12. (function() {
  13.  
  14. 'use strict';
  15.  
  16. console.log('[HF] Crime Profatibility script running');
  17.  
  18. let emforusData = `https://docs.google.com/spreadsheets/d/13wUFhhssuPdAONI_OmRJi6l_Bs7KRZXDgVFCn7uJJNQ/gviz/tq?tqx=out:csv&gid=560321570`;
  19. let crackingData = `https://docs.google.com/spreadsheets/d/13wUFhhssuPdAONI_OmRJi6l_Bs7KRZXDgVFCn7uJJNQ/gviz/tq?tqx=out:csv&gid=1626436424`;
  20.  
  21. let rememberedData = JSON.parse(localStorage.getItem('hf-crime-profitability-data'));
  22. let rememberedCrackingData = JSON.parse(localStorage.getItem('hf-crime-profitability-cracking-data'));
  23. let lastFetched = Number(localStorage.getItem('hf-crime-profitability-last-fetched'));
  24. let rememberedBFS = Number(localStorage.getItem('hf-crime-profitability-bfs'));
  25.  
  26. let cachedData = null;
  27. let cachedCrackingData = null;
  28. let bfs = 0;
  29. let currentHref = window.location.href;
  30.  
  31. let settings = {};
  32. let savedSettings = JSON.parse(localStorage.getItem('hf-crime-profitability-settings'));
  33. if (savedSettings) settings = savedSettings;
  34.  
  35. let threshold = 0;
  36. let savedThreshold = Number(localStorage.getItem('hf-crime-profitability-threshold'));
  37. if (savedThreshold) threshold = savedThreshold;
  38.  
  39. // MAKE PDA COMPATIBLE
  40. let pda = ('xmlhttpRequest' in GM);
  41. let httpRequest = pda ? 'xmlhttpRequest' : 'xmlHttpRequest';
  42.  
  43. // Display value per nerve on search for most crimes
  44. function crimePage(data, crackingData, observed, retries = 30) {
  45. let list = document.body.querySelector('.virtualList___noLef');
  46. let subCrimes = list?.querySelectorAll('.crimeOptionSection___hslpu');
  47.  
  48. // If DOM isn't fully loaded, try again
  49. if (!list || !subCrimes || subCrimes.length < 2) {
  50. if (retries > 0) {
  51. setTimeout(() => crimePage(data, crackingData, observed, retries - 1), 100);
  52. } else {
  53. console.warn('[HF] Gave up looking for Crime Page subtitles after 30 retries.');
  54. }
  55. return;
  56. }
  57.  
  58. let maximum = -Infinity, maximumElement = null;
  59.  
  60. // SPECIAL CHANGES FOR SPECIFIC CRIMES //
  61.  
  62. // Create an observer on the pickpocketing page, as new targets are added and old ones removed
  63. if (window.location.href.includes('pickpocketing') && !observed) createObserver(list, data);
  64.  
  65. // Create an observer on the cracking page for scrolling reasons
  66. if (window.location.href.includes('cracking')) {
  67. if (!observed) createObserver(list, data, crackingData);
  68. let rig = document.body.querySelector('.strength___DM3lW .value___FmWPr');
  69.  
  70. // If the page hasn't fully loaded yet, try again
  71. if (!rig) {
  72. findCrackingBFS();
  73. }
  74. }
  75.  
  76. // Create an observer on the forgery page for when new projects are begun
  77. if (window.location.href.includes('forgery') && !observed) createObserver(list, data);
  78.  
  79. // Create an observer on the burglary page for when new projects are begun
  80. if (window.location.href.includes('burglary') && !observed) createObserver(list, data);
  81.  
  82. // FORGERY: dropdown container on top of the page
  83. let forgeryPage = window.location.href.includes('forgery');
  84. let maximumForgery = -Infinity, maximumForgeryTarget = null;
  85.  
  86. // CRACKING: check BFS as $/N differs on it
  87. if (rememberedBFS) bfs = rememberedBFS; // If rig not found, rely on previous bfs info for now
  88. let rig = document.body.querySelector('.strength___DM3lW .value___FmWPr');
  89. if (rig) {
  90. bfs = Math.round(parseFloat(rig.textContent.trim()));
  91. localStorage.setItem('hf-crime-profitability-bfs', bfs);
  92. }
  93.  
  94. // BURGLARY
  95. let burglaryPage = window.location.href.includes('burglary');
  96.  
  97. // BACK TO THE MAIN CODE //
  98.  
  99. // Create a datamap for each subcrime
  100. let dataMap = new Map();
  101. for (let crimeData of data) {
  102. if (burglaryPage) {
  103. // SPECIAL CHANGES FOR BURLGARY: focused vs optimal
  104. if (!crimeData.target) continue;
  105. if (crimeData.crime !== 'Burglary') continue;
  106.  
  107. if (crimeData.target.includes('Focused')) {
  108. dataMap.set(crimeData.target.replace(' (Focused)', '').toLowerCase(), crimeData);
  109. } else if (crimeData.target.includes('Average')) {
  110. dataMap.set(`any ${crimeData.target.replace(' (Average)', '')}`.toLowerCase(), crimeData);
  111. }
  112. } else {
  113. if (crimeData.target === 'City Centre') crimeData.target = 'City Center'; // Issue in the sheet for graffiti
  114. if (crimeData.target) dataMap.set(crimeData.target.toLowerCase(), crimeData);
  115.  
  116. // If forgery, find the best forgery target!
  117. if (forgeryPage && crimeData.crime === 'Forgery' && parseInt(crimeData['$/N']?.replace(/[$,]/g, '')) > maximumForgery) {
  118. maximumForgery = parseInt(crimeData['$/N']?.replace(/[$,]/g, ''));
  119. maximumForgeryTarget = crimeData.target;
  120. }
  121. }
  122. }
  123.  
  124. // SPECIAL CHANGES FOR SPECIFIC CRIMES //
  125.  
  126. // Create a CRACKING separate datamap due to separate data sheet
  127. let crackingDataMap = new Map();
  128. if (window.location.href.includes('cracking') && crackingData) {
  129. for (let crimeData of crackingData) {
  130. if (crimeData['Targeted service ']) crackingDataMap.set(crimeData['Targeted service '].toLowerCase(), crimeData);
  131. }
  132. }
  133.  
  134. // BACK TO THE MAIN CODE //
  135.  
  136. for (let subCrime of subCrimes) {
  137. let firstTextNode = Array.from(subCrime.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
  138. let label = firstTextNode?.textContent.trim().toLowerCase();
  139.  
  140. // SPECIAL CHANGES FOR SPECIFIC CRIMES //
  141.  
  142. // PICKPOCKETING
  143. let pickpocketing = subCrime.querySelector('.titleAndProps___DdeVu div');
  144. if (pickpocketing) label = pickpocketing.textContent.trim().toLowerCase();
  145.  
  146. // CRACKING
  147. let cracking = subCrime.querySelector('.type___T9oMA');
  148. let crackingService = subCrime.querySelector('.service___uYhDL');
  149.  
  150.  
  151. // FORGERY (MOBILE)
  152. let mobileForgery = subCrime.querySelector('.tabletProjectTitle___Wsdf7');
  153. if (mobileForgery) {
  154. label = mobileForgery.textContent.trim().toLowerCase();
  155. firstTextNode = Array.from(mobileForgery.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
  156. }
  157.  
  158. // FORGERY
  159. if (forgeryPage) {
  160. let dropdownWrapper = subCrime.querySelector('.optionWithLevelRequirement___cHH35');
  161. if ((!mobileForgery && subCrime.textContent === 'Begin a New Project') || dropdownWrapper) {
  162. forgery(subCrime, dataMap, maximumForgery, maximumForgeryTarget);
  163. }
  164. }
  165.  
  166. if (burglaryPage) {
  167. let dropdownWrapper = subCrime.querySelector('.dropdownWrapper___Ij6CY');
  168. if (dropdownWrapper) burglary(subCrime, dataMap);
  169. }
  170.  
  171. // SEARCH FOR CASH (MOBILE)
  172. let mobileSearchForCash = subCrime.querySelector('.titleAndIcon___h8RJV');
  173. if (mobileSearchForCash) {
  174. firstTextNode = Array.from(mobileSearchForCash.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
  175. label = firstTextNode?.textContent.trim().toLowerCase();
  176. }
  177.  
  178. // GRAFFITI (MOBILE)
  179. let mobileGraffiti = subCrime.querySelector('.tabletTitleAndTagCount___vb0UQ');
  180. if (mobileGraffiti) {
  181. firstTextNode = Array.from(mobileGraffiti.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
  182. label = firstTextNode?.textContent.trim().toLowerCase();
  183. }
  184.  
  185. // SHOPLIFTING (MOBILE)
  186. let mobileShoplifting = subCrime.querySelector('.tabletShopTitle___aqRuE');
  187. if (mobileShoplifting) label = mobileShoplifting.textContent.trim().toLowerCase();
  188.  
  189. // MOBILE MISC
  190. let mobileOther = document.body.querySelector('.area-mobile___BH0Ku');
  191.  
  192. // MOBILE BURLGARY
  193. if (mobileOther && burglaryPage) {
  194. let title = subCrime.querySelector('.title___kOWyb');
  195. if (title) label = title.textContent.trim().toLowerCase();
  196. if (label === 'farm storage') label = 'farm storage unit'; // Unit gets removed on mobile
  197. }
  198.  
  199. // BACK TO THE MAIN CODE //
  200. let crimeData = dataMap.get(label);
  201.  
  202. // SPECIAL CHANGES FOR SPECIFIC CRIMES //
  203.  
  204. // CRACKING
  205. if (cracking) {
  206. label = crackingService.textContent.trim().toLowerCase();
  207. crimeData = crackingDataMap.get(label);
  208.  
  209. if (bfs === 6) {
  210. crimeData['$/N'] = crimeData['Estimated profit per nerve 6BFS'];
  211. } else if (bfs === 7) {
  212. crimeData['$/N'] = crimeData['7BFS'];
  213. } else {
  214. crimeData['$/N'] = crimeData['Estimated profit per nerve 6BFS'];
  215. }
  216. }
  217.  
  218. // GRAFFITI (MOBILE)
  219. if (mobileGraffiti && !crimeData) crimeData = dataMap.get(`${label} district`);
  220.  
  221. // If crime data does not exist (due to wrong element or other reason), skip the rest
  222. if (!crimeData) continue;
  223.  
  224. // STYLE CHANGES
  225. subCrime.style.display = 'flex';
  226.  
  227. // MOBILE SEARCH FOR CASH AND SHOPLIFTING EXTRA STILE CHANGES
  228. if (!mobileSearchForCash && !mobileShoplifting) subCrime.style.justifyContent = 'space-between';
  229.  
  230. // MOBILE STYLE CHANGES
  231. let typeServiceWrapper = subCrime.querySelector('.typeAndServiceWrapper___AoONK');
  232. if (typeServiceWrapper) typeServiceWrapper.style.flex = '1';
  233.  
  234.  
  235. // BACK TO THE MAIN CODE //
  236.  
  237. // CREATE VALUE SPAN
  238. let span = document.createElement('span');
  239. span.classList.add('hf-value');
  240.  
  241. let moneyPerNerve = -Infinity;
  242.  
  243. // FILL IN VALUE DATA IN SPAN
  244. if (crimeData['$/N']) { // Information found in the sheet
  245. moneyPerNerve = parseInt(crimeData['$/N']?.replace(/[$,]/g, ''));
  246.  
  247. // Find the maximum money per nerve for this crime!
  248. if (moneyPerNerve > maximum) {
  249. maximum = moneyPerNerve
  250. maximumElement = subCrime.parentNode;
  251. }
  252.  
  253. let spanText = `${moneyPerNerve < 0 ? '-' : ''}$${Math.abs(moneyPerNerve).toLocaleString('en-US')} / N`;
  254. span.textContent = spanText;
  255.  
  256. // Color green by default, but red if negative $/N and yellow if below threshold but above 0
  257. span.style.color = 'var(--default-base-green-color)';
  258. if (moneyPerNerve < threshold) span.style.color = 'var(--default-base-gold-color)';
  259. if (moneyPerNerve < 0) span.style.color = 'var(--default-base-important-color)';
  260.  
  261. // SPECIAL TOOLTIP FOR MOBILE GRAFFITI AND SHOPLIFTING
  262. if (mobileGraffiti) mobileGraffiti.title = spanText;
  263. if (mobileShoplifting) mobileShoplifting.title = spanText;
  264.  
  265. } else {
  266. let spanText = '$/N N/A';
  267. span.textContent = spanText;
  268.  
  269. // Color yellow for N/A
  270. span.style.color = 'var(--default-base-gold-color)';
  271.  
  272. // SPECIAL TOOLTIP FOR MOBILE GRAFFITI AND SHOPLIFTING
  273. if (mobileGraffiti) mobileGraffiti.title = spanText;
  274. if (mobileShoplifting) mobileShoplifting.title = spanText;
  275. }
  276.  
  277. // Set attribute to the item container, so I can easily use that later
  278. subCrime.parentNode.parentNode.parentNode.parentNode.setAttribute('data-hf-value', moneyPerNerve);
  279.  
  280. // If the SPAN doesn't exist yet, append it! DIFFERENT FOR SPECIAL CRIMES
  281. let existingSpan = subCrime.querySelector('.hf-value');
  282. if (!existingSpan) {
  283. if (cracking) {
  284. subCrime.appendChild(span);
  285. } else if (mobileSearchForCash) {
  286. mobileSearchForCash.insertBefore(span, mobileSearchForCash.childNodes[1] || null);
  287. } else if (mobileOther && burglaryPage) {
  288. let titleAndProgress = subCrime.querySelector('.titleAndProgress___pukj7');
  289. let progressBar = titleAndProgress.querySelector('.progressBar___JhMrP');
  290. titleAndProgress.insertBefore(span, progressBar);
  291. } else if (!forgeryPage && !mobileGraffiti && !mobileShoplifting) {
  292. subCrime.insertBefore(span, subCrime.childNodes[1] || null);
  293. }
  294. }
  295.  
  296.  
  297. // SPECIAL STYLE CHANGES FOR SPECIFIC (MOBILE) CRIMES //
  298.  
  299. let graffiti = subCrime.querySelector('.reputationIconWrapper___CM05s');
  300. if (graffiti || pickpocketing) span.style.paddingLeft = '15px';
  301. if (pickpocketing) span.style.flex = '2';
  302. if (mobileOther && pickpocketing) span.style.fontSize = 'smaller';
  303. if (burglaryPage && !mobileOther) {
  304. span.style.marginLeft = 'auto';
  305. span.style.marginRight = '8px';
  306.  
  307. let abandonButtonWrapper = subCrime.querySelector('.abandonButtonWrapper___qQOAG');
  308. abandonButtonWrapper.style.marginLeft = '0px';
  309. }
  310.  
  311. // On forgery, take care of the dropdown/overview container
  312. if (forgeryPage && !mobileForgery) {
  313. let div = document.createElement('div');
  314. div.style.display = 'flex';
  315. div.style.flexDirection = 'column';
  316.  
  317. let typeSpan = document.createElement('span');
  318. typeSpan.textContent = firstTextNode.textContent;
  319.  
  320. div.appendChild(typeSpan);
  321. div.appendChild(span);
  322.  
  323. span.style.paddingTop = '5px';
  324.  
  325. subCrime.insertBefore(div, subCrime.childNodes[1] || null);
  326.  
  327. firstTextNode.remove();
  328. } else if (mobileForgery) {
  329. mobileForgery.appendChild(span);
  330. span.style.paddingLeft = '5px';
  331. }
  332.  
  333. subCrime.parentNode.style.background = ''; // So crimes like pickpocketing aren't always showing multiple options
  334. }
  335.  
  336.  
  337. // BACK TO THE MAIN CODE //
  338.  
  339. if (maximumElement && maximum > threshold) maximumElement.style.background = 'var(--default-bg-green-hover-color)';
  340.  
  341. // DON'T SHOW SORT BUTTON ON PICKPOCKETING
  342. if (window.location.href.includes('pickpocketing')) return; // Button is just too hard with the way the crime works
  343.  
  344. // CREATE A SORT BUTTON
  345. let existingButtons = document.body.querySelectorAll('.hf-sort-button');
  346. if (existingButtons) {
  347. for (let button of existingButtons) {
  348. button.remove();
  349. }
  350. }
  351.  
  352. let button = createSortButton();
  353. button.addEventListener('click', function () {
  354. let invalidOpposite = false;
  355. if (forgeryPage || burglaryPage) invalidOpposite = true; // FORGERY has an overview as first "subcrime"
  356. sortButtonClick(list, button, invalidOpposite);
  357. });
  358. }
  359.  
  360. // HELPER FUNCTION to keep looking for the rig on CRACKING
  361. function findCrackingBFS(retries = 60) {
  362. if (!window.location.href.includes('cracking')) return; // Don't keep running if the user has gone away from the cracking page
  363.  
  364. let rig = document.body.querySelector('.strength___DM3lW .value___FmWPr');
  365. if (rig) {
  366. bfs = Math.round(parseFloat(rig.textContent.trim()));
  367. localStorage.setItem('hf-crime-profitability-bfs', bfs);
  368. return;
  369. }
  370.  
  371. if (retries > 0) {
  372. setTimeout(() => findCrackingBFS(retries - 1), 1000);
  373. } else {
  374. console.warn('[HF] Gave up looking for Cracking Rig after 60 tries (1 per second).');
  375. }
  376. }
  377.  
  378. // Display value per nerve on BOOTLEGGING
  379. function bootlegging(data, retries = 30) {
  380. let optionWrappers = document.body.querySelectorAll('.crimeOptionWrapper___IOnLO');
  381.  
  382. // If the DOM hasn't loaded yet, try again!
  383. if (!optionWrappers || optionWrappers.length < 2) {
  384. if (retries > 0) {
  385. setTimeout(() => bootlegging(data, retries - 1), 100);
  386. } else {
  387. console.warn('[HF] Gave up looking for Bootlegging subtitles after 30 retries.');
  388. }
  389. return;
  390. }
  391.  
  392. // Map of UI labels to normalized data targets
  393. let labelToTargetMap = {
  394. 'sell counterfeit dvds': 'sell counterfeit dvds',
  395. 'online store': 'collect from online store',
  396. };
  397.  
  398. let maximum = -Infinity;
  399. let maximumElement = null;
  400.  
  401. for (let optionWrapper of optionWrappers) {
  402. // Find the "Sell" and "Online" buttons
  403. if (!optionWrapper.textContent.toLowerCase().includes('sell') && !optionWrapper.textContent.toLowerCase().includes('online')) continue;
  404.  
  405. let options = optionWrapper.querySelectorAll('.crimeOptionSection___hslpu');
  406. for (let option of options) {
  407. let label = option.textContent.trim().toLowerCase();
  408. let target = labelToTargetMap[label];
  409.  
  410. let moneyPerNerve = -Infinity;
  411.  
  412. // If target can't be found, move on
  413. if (!target) continue;
  414.  
  415. let match = data.find(c => c.target.toLowerCase() === target);
  416. if (match && match['$/N']) moneyPerNerve = parseInt(match['$/N'].replace('$', ''));
  417.  
  418. // Create the span
  419. let span = document.createElement('span');
  420. span.classList.add('hf-value');
  421. span.textContent = `${moneyPerNerve < 0 ? '-' : ''}$${Math.abs(moneyPerNerve).toLocaleString('en-US')} / N`;
  422. span.style.paddingLeft = '15px';
  423. span.style.color = 'var(--default-base-green-color)';
  424. if (moneyPerNerve < threshold) span.style.color = 'var(--default-base-gold-color)';
  425. if (moneyPerNerve < 0) span.style.color = 'var(--default-base-important-color)';
  426.  
  427. if (moneyPerNerve > maximum) {
  428. maximum = moneyPerNerve
  429. maximumElement = option.parentNode;
  430. }
  431.  
  432. option.appendChild(span);
  433. }
  434. }
  435.  
  436. if (maximum > threshold) maximumElement.style.background = 'var(--default-bg-green-hover-color)';
  437. }
  438.  
  439. // Display value per nerve on DISPOSAL
  440. function disposal(data, observed, retries = 30) {
  441. let mobile = document.body.querySelector('.area-mobile___BH0Ku');
  442. let list = document.body.querySelector('.virtualList___noLef');
  443. let subCrimes = list?.querySelectorAll('.crimeOptionSection___hslpu');
  444.  
  445. // If the DOM hasn't fully loaded yet, try again
  446. if (!list || !subCrimes || subCrimes.length < 2) {
  447. if (retries > 0) {
  448. setTimeout(() => disposal(data, observed, retries - 1), 100);
  449. } else {
  450. console.warn('[HF] Gave up looking for Disposal subtitles after 30 retries.');
  451. }
  452. return;
  453. }
  454.  
  455. if (!observed) {
  456. createObserver(list, data, crackingData, true);
  457. }
  458.  
  459. let disposalData = {};
  460.  
  461.  
  462. for (let crimeData of data) {
  463. // If crime isn't disposal, no need to loop through it
  464. if (crimeData.crime !== 'Disposal') continue;
  465.  
  466. // Disposal is formatted in the sheet as "Subtitle: Type"
  467. let [targetTitle, targetType] = crimeData.target.split(':').map(s => s.trim());
  468. if (!disposalData[targetTitle.trim().toLowerCase()]) {
  469. disposalData[targetTitle.trim().toLowerCase()] = {};
  470. }
  471.  
  472. disposalData[targetTitle.trim().toLowerCase()][targetType] = parseInt(crimeData['$/N']?.replace(/[$,]/g, ''));
  473. }
  474.  
  475. let maximum = -Infinity;
  476. let maximumElement = null;
  477.  
  478. for (let subCrime of subCrimes) {
  479. let firstTextNode = Array.from(subCrime.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
  480. let label = firstTextNode?.textContent.trim().toLowerCase();
  481.  
  482. let data = disposalData[label]
  483. if (!data) continue; // If data is not found, break it off here
  484.  
  485. let maximumValue = -Infinity;
  486. let maximumType = null;
  487.  
  488. for (let type in data) {
  489. if (data[type] > maximumValue) {
  490. maximumValue = data[type];
  491. maximumType = type;
  492. }
  493. }
  494.  
  495. // Take care of the method buttons
  496. let methods = subCrime.parentNode.querySelectorAll('.methodButton___lCgpf');
  497. for (let method of methods) {
  498. let ariaLabel = method.getAttribute('aria-label');
  499. let info = disposalData[label][ariaLabel.trim()];
  500.  
  501. // Display $/N upon hover
  502. method.title = `${info < 0 ? '-' : ''}$${Math.abs(info).toLocaleString('en-US')} / N`;
  503.  
  504. // Give the method borders based on profitability
  505. if (info < 0) method.style.border = '1px solid var(--default-base-important-color)';
  506. if (info >= 0) method.style.border = '1px solid var(--default-base-gold-color)';
  507. if (ariaLabel.trim().toLowerCase() === maximumType.toLowerCase()) method.style.border = '1px solid var(--default-base-green-color)';
  508. }
  509.  
  510. if (maximumValue > maximum) {
  511. maximum = maximumValue
  512. maximumElement = subCrime.parentNode.parentNode;
  513. }
  514.  
  515. // Create value div
  516. let div = document.createElement('div');
  517. div.classList.add('hf-value');
  518. div.style.display = 'flex';
  519. div.style.flexDirection = 'column';
  520. div.style.alignItems = 'flex-end';
  521. div.style.color = 'var(--default-base-green-color)';
  522. if (maximumValue < threshold) div.style.color = 'var(--default-base-gold-color)';
  523. if (maximumValue < 0) div.style.color = 'var(--default-base-important-color)';
  524.  
  525. let type = document.createElement('span');
  526. type.textContent = maximumType;
  527. div.appendChild(type);
  528.  
  529. let value = document.createElement('span');
  530. value.textContent = `${maximumValue < 0 ? '-' : ''}$${Math.abs(maximumValue).toLocaleString('en-US')} / N`;
  531. div.appendChild(value);
  532.  
  533. let existingDiv = subCrime.querySelector('.hf-value');
  534. if (!existingDiv) subCrime.appendChild(div);
  535. subCrime.style.display = 'flex';
  536. subCrime.style.justifyContent = 'space-between';
  537.  
  538. // MOBILE STYLE CHANGES
  539. if (mobile) {
  540. subCrime.style.flexWrap = 'wrap';
  541. subCrime.style.alignContent = 'center';
  542. div.style.paddingTop = '5px';
  543. div.style.flexDirection = '';
  544. type.textContent += ':';
  545. value.style.paddingLeft = '5px';
  546. }
  547.  
  548. subCrime.parentNode.parentNode.parentNode.parentNode.setAttribute('data-hf-value', maximumValue);
  549.  
  550. subCrime.parentNode.parentNode.style.background = ''; // So it isn't showing multiple options upon page refresh
  551. }
  552.  
  553. if (maximumElement && maximum > threshold) maximumElement.style.background = 'var(--default-bg-green-hover-color)';
  554.  
  555. let existingButtons = document.body.querySelectorAll('.hf-sort-button');
  556. if (existingButtons) {
  557. for (let button of existingButtons) {
  558. button.remove();
  559. }
  560. }
  561.  
  562. let button = createSortButton();
  563. button.addEventListener('click', function () {
  564. sortButtonClick(list, button);
  565. });
  566. }
  567.  
  568. // HELPER FUNCTION for the dropdown and "Begin a New Project" in FORGERY
  569. function forgery(subCrime, dataMap, maximum, maximumTarget) {
  570. let mobileForgery = document.body.querySelector('.tabletProjectTitle___Wsdf7');
  571. let mobile = document.body.querySelector('.area-mobile___BH0Ku');
  572.  
  573. if (subCrime.textContent === 'Begin a New Project') {
  574. if (mobile) return; // No "Begin a New Project" title on mobile
  575.  
  576. // Don't just keep adding info!
  577. let existingDiv = document.body.querySelector('.hf-best-forgery');
  578.  
  579. let div = document.createElement('div');
  580. div.classList.add('hf-best-forgery');
  581. div.style.display = 'flex';
  582. div.style.alignItems = 'flex-end';
  583. div.style.color = 'var(--default-base-green-color)';
  584. if (maximum < threshold) div.style.color = 'var(--default-base-gold-color)';
  585. if (maximum < 0) div.style.color = 'var(--default-base-important-color)';
  586.  
  587. let maximumTargetEl = document.createElement('span');
  588. maximumTargetEl.textContent = `${maximumTarget}`;
  589.  
  590. let maximumEl = document.createElement('span');
  591. maximumEl.textContent = `(${maximum < 0 ? '-' : ''}$${Math.abs(maximum).toLocaleString('en-US')} / N)`;
  592. maximumEl.style.paddingLeft = '5px';
  593.  
  594. div.appendChild(maximumTargetEl);
  595. div.appendChild(maximumEl);
  596.  
  597. subCrime.style.display = 'flex';
  598. subCrime.style.justifyContent = 'space-between';
  599. if (!existingDiv) subCrime.appendChild(div);
  600.  
  601. return;
  602. }
  603.  
  604. let dropdownWrapper = subCrime.querySelector('.optionWithLevelRequirement___cHH35');
  605. if (dropdownWrapper) {
  606. let typesEl = dropdownWrapper.lastChild;
  607.  
  608. // Don't just keep adding info!
  609. let existingMainSpan = document.body.querySelector('.hf-selected-dropdown-value');
  610. if (existingMainSpan) return;
  611.  
  612. let mainSpan = document.createElement('span');
  613. mainSpan.classList.add('hf-selected-dropdown-value');
  614. mainSpan.textContent = ` (${maximum < 0 ? '-' : ''}$${Math.abs(maximum).toLocaleString('en-US')} / N)`;
  615. mainSpan.style.display = 'contents';
  616. mainSpan.style.color = 'var(--default-base-green-color)';
  617. if (mainSpan < threshold) mainSpan.style.color = 'var(--default-base-gold-color)';
  618. if (maximum < 0) mainSpan.style.color = 'var(--default-base-important-color)';
  619.  
  620. dropdownWrapper.appendChild(mainSpan);
  621.  
  622. let ul = subCrime.querySelector('ul');
  623. let lists = ul.querySelectorAll('li');
  624. for (let list of lists) {
  625. // Don't just keep adding info!
  626. let existingSpan = list.querySelector('.hf-dropdown-value');
  627. if (existingSpan) continue;
  628.  
  629. let id = list.id;
  630. id = id.replace(/-/g, ' ').replace(/\d+$/, '').replace('option', '');
  631.  
  632. let crimeData = dataMap.get(id.trim().toLowerCase());
  633. if (!crimeData['$/N']) continue;
  634.  
  635. let option = list.querySelector('.optionWithLevelRequirement___cHH35');
  636. let listExplanation = option.lastChild;
  637.  
  638. let value = parseInt(crimeData['$/N']?.replace(/[$,]/g, ''));
  639.  
  640. // Create a green span for the value
  641. let span = document.createElement('span');
  642. span.classList.add('hf-dropdown-value');
  643. span.style.display = 'contents';
  644. span.style.color = 'var(--default-base-green-color)';
  645. if (value < threshold) span.style.color = 'var(--default-base-gold-color)';
  646. if (value < 0) span.style.color = 'var(--default-base-important-color)';
  647.  
  648. let amount = parseInt(crimeData['$/N']?.replace(/[$,]/g, ''));
  649. span.textContent = ` (${amount < 0 ? '-' : ''}$${Math.abs(amount).toLocaleString('en-US')})`;
  650.  
  651. option.appendChild(span);
  652.  
  653. continue;
  654. }
  655. }
  656. }
  657.  
  658. // HELPER FUNCTION for the dropdown and category options in BURGLARY
  659. function burglary(subCrime, dataMap, listening) {
  660. let bestOptionName = null;
  661. let bestOptionValue = null;
  662.  
  663. for (let [key, value] of dataMap.entries()) {
  664. let currentValue = parseFloat(value["$/N"].replace(/[\$,]/g, ''));
  665.  
  666. if (currentValue > bestOptionValue) {
  667. bestOptionValue = currentValue;
  668. bestOptionName = value.target;
  669. }
  670. }
  671.  
  672. let mobile = document.body.querySelector('.area-mobile___BH0Ku');
  673.  
  674. if (listening) subCrime = document.body.querySelector('.propertyTypeSection___Hw3kk');
  675.  
  676. let categoryButtons = subCrime.querySelectorAll('.targetCategoryButtons___tczX4');
  677. for (let categoryButton of categoryButtons) {
  678. if (listening) break;
  679.  
  680. categoryButton.addEventListener('click', function() {
  681. setTimeout(() => burglary(subCrime, dataMap, true), 100);
  682. });
  683. }
  684.  
  685. if (!mobile) {
  686. // Don't just keep adding info!
  687. let existingDiv = document.body.querySelector('.hf-best-burglary');
  688.  
  689. let div = document.createElement('div');
  690. div.classList.add('hf-best-burglary');
  691. div.style.display = 'flex';
  692. div.style.alignItems = 'flex-end';
  693. div.style.color = 'var(--default-base-green-color)';
  694. div.style.flexDirection = 'column';
  695. if (bestOptionValue < threshold) div.style.color = 'var(--default-base-gold-color)';
  696. if (bestOptionValue < 0) div.style.color = 'var(--default-base-important-color)';
  697.  
  698. let maximumTargetEl = document.createElement('span');
  699. maximumTargetEl.textContent = `${bestOptionName}`;
  700.  
  701. let maximumEl = document.createElement('span');
  702. maximumEl.textContent = `(${bestOptionValue < 0 ? '-' : ''}$${Math.abs(bestOptionValue).toLocaleString('en-US')} / N)`;
  703. maximumEl.style.paddingLeft = '5px';
  704. maximumEl.style.padingTop = '3px';
  705.  
  706. div.appendChild(maximumTargetEl);
  707. div.appendChild(maximumEl);
  708.  
  709. let categoryButtonsWrapper = subCrime.querySelector('.targetCategoryButtons___tczX4');
  710. if (!existingDiv) subCrime.insertBefore(div, categoryButtonsWrapper);
  711. }
  712.  
  713. // Change every time the button is clicked to change
  714. let dropdownWrapper = document.body.querySelector('.dropdownMainWrapper___PjDiT button');
  715. let selectedDropdown = dropdownWrapper.textContent.toLowerCase();
  716.  
  717. let crimeData = dataMap.get(selectedDropdown);
  718.  
  719. let moneyPerNerve = 0;
  720. if (crimeData && crimeData['$/N']) { // Information found in the sheet
  721. moneyPerNerve = parseInt(crimeData['$/N']?.replace(/[$,]/g, ''));
  722. }
  723.  
  724. // Don't just keep adding info!
  725. let existingSpan = document.body.querySelector('.hf-dropdown-value');
  726. if (existingSpan) return;
  727.  
  728. let existingMainSpan = document.body.querySelector('.hf-selected-dropdown-value');
  729. if (existingMainSpan) {
  730. existingMainSpan.remove();
  731. }
  732.  
  733. let mainSpan = document.createElement('span');
  734. mainSpan.classList.add('hf-selected-dropdown-value');
  735. mainSpan.textContent = ` (${moneyPerNerve < 0 ? '-' : ''}$${Math.abs(moneyPerNerve).toLocaleString('en-US')} / N)`;
  736. mainSpan.style.display = 'contents';
  737. mainSpan.style.color = 'var(--default-base-green-color)';
  738. if (mainSpan < threshold) mainSpan.style.color = 'var(--default-base-gold-color)';
  739. if (moneyPerNerve < 0) mainSpan.style.color = 'var(--default-base-important-color)';
  740.  
  741. if (mobile) {
  742. mainSpan.style.display = 'flex';
  743. dropdownWrapper.style.display = 'flex';
  744. dropdownWrapper.style.flexDirection = 'column';
  745. dropdownWrapper.style.alignItems = 'flex-start';
  746. dropdownWrapper.style.paddingTop = '3px';
  747. }
  748.  
  749. dropdownWrapper.appendChild(mainSpan);
  750.  
  751. let ul = subCrime.querySelector('ul');
  752.  
  753. let lists = ul.querySelectorAll('li');
  754. for (let list of lists) {
  755. // Don't just keep adding info!
  756. let existingSpan = list.querySelector('.hf-dropdown-value');
  757. if (existingSpan) continue;
  758.  
  759. let id = list.id;
  760. id = id.replace(/-/g, ' ').replace(/\d+$/, '').replace('option', '');
  761.  
  762. let crimeData = dataMap.get(id.trim().toLowerCase());
  763.  
  764. if (!crimeData && id.trim() === 'Self Storage Facility') {
  765. // Due to an issue Torn vs Sheet
  766. id = 'Self-Storage Facility';
  767.  
  768. crimeData = dataMap.get(id.trim().toLowerCase())
  769. }
  770.  
  771. if (!crimeData['$/N']) continue;
  772.  
  773. let item = list.querySelector('.dropdownItem___qL6_Y');
  774.  
  775. let value = parseInt(crimeData['$/N']?.replace(/[$,]/g, ''));
  776.  
  777. // Create a green span for the value
  778. let span = document.createElement('span');
  779. span.classList.add('hf-dropdown-value');
  780. span.style.display = 'contents';
  781. span.style.color = 'var(--default-base-green-color)';
  782. if (value < threshold) span.style.color = 'var(--default-base-gold-color)';
  783. if (value < 0) span.style.color = 'var(--default-base-important-color)';
  784.  
  785. let amount = parseInt(crimeData['$/N']?.replace(/[$,]/g, ''));
  786. span.textContent = ` (${amount < 0 ? '-' : ''}$${Math.abs(amount).toLocaleString('en-US')})`;
  787.  
  788. if (mobile) {
  789. span.style.display = 'flex';
  790. item.style.display = 'flex';
  791. item.style.flexDirection = 'column';
  792. item.style.alignItems = 'flex-start';
  793. item.style.paddingTop = '3px';
  794. }
  795.  
  796.  
  797. item.appendChild(span);
  798.  
  799. continue;
  800. }
  801. }
  802.  
  803. // HELPER FUNCTION to create the sort button
  804. function createSortButton() {
  805. // Remove any leftover buttons!
  806. let existingButtons = document.body.querySelectorAll('.hf-sort-button');
  807. if (existingButtons) {
  808. for (let button of existingButtons) {
  809. button.remove();
  810. }
  811. }
  812.  
  813. let headerElement = document.body.querySelector('.crimes-app-header');
  814.  
  815. let button = document.createElement('button');
  816. button.textContent = '▲ Profitability';
  817. button.classList.add('hf-sort-button');
  818. button.style.background = 'var(--input-money-error-border-color)';
  819. button.style.color = 'var(--btn-color)';
  820. button.style.borderRadius = '8px';
  821. button.style.cursor = 'pointer';
  822. button.style.fontWeight = 'bold';
  823.  
  824. headerElement.insertBefore(button, headerElement.lastElementChild);
  825.  
  826. return button;
  827. }
  828.  
  829. // HELPER FUNCTION to call when the sort button is clicked
  830. function sortButtonClick(list, button, invalidOpposite) {
  831. let subCrimeArray = Array.from(list.querySelectorAll('.virtual-item'));
  832.  
  833. subCrimeArray.sort((a, b) => {
  834. let aRaw = a.getAttribute('data-hf-value');
  835. let bRaw = b.getAttribute('data-hf-value');
  836.  
  837. let aVal = parseFloat(aRaw);
  838. let bVal = parseFloat(bRaw);
  839.  
  840. let aInvalid = isNaN(aVal);
  841. let bInvalid = isNaN(bVal);
  842.  
  843. // Push invalid (missing or NaN) values to the bottom or top depending on the crime
  844. if (invalidOpposite && (!a.classList.contains('virtualItemsBackdrop___oTwUm') || !b.classList.contains('virtualItemsBackdrop___oTwUm'))) {
  845. if (aInvalid && !bInvalid) return -1;
  846. if (!aInvalid && bInvalid) return 1;
  847. if (aInvalid && bInvalid) return 0;
  848. } else {
  849. if (aInvalid && !bInvalid) return 1;
  850. if (!aInvalid && bInvalid) return -1;
  851. if (aInvalid && bInvalid) return 0;
  852. }
  853.  
  854. if (button.textContent.includes('▲')) {
  855. return bVal - aVal; // highest first
  856. } else {
  857. return aVal - bVal; // lowest first
  858. }
  859. });
  860.  
  861. // Reposition elements visually
  862. subCrimeArray.forEach((el, i) => {
  863. if (invalidOpposite) {
  864. if (i === 0) {
  865. return;
  866. } else if (i === 1) {
  867. el.style.transform = `translateY(64px)`;
  868. return;
  869. }
  870.  
  871. el.style.transform = `translateY(${((i - 1) * 51) + 64}px)`;
  872. } else {
  873. el.style.transform = `translateY(${i * 51}px)`;
  874. }
  875. });
  876.  
  877. let backDrop = list.querySelector('.virtualItemsBackdrop___oTwUm');
  878.  
  879. // Toggle sort direction
  880. button.textContent = button.textContent.includes('▲') ? '▼ Profitability' : '▲ Profitability';
  881. }
  882.  
  883. // BURGLARY create sheets button
  884. function createSheetsButton() {
  885. // Don't just keep adding buttons!
  886. let existingButton = document.body.querySelector('.hf-sheets-button');
  887. if (existingButton) return;
  888.  
  889. let headerElement = document.body.querySelector('.crimes-app-header');
  890.  
  891. let button = document.createElement('button');
  892. button.textContent = 'Google Sheets';
  893. button.title = `Crime Profitability will be added soonTM for this crime (data pending)`;
  894. button.classList.add('hf-sheets-button');
  895. button.style.background = 'var(--input-money-error-border-color)';
  896. button.style.color = 'var(--btn-color)';
  897. button.style.borderRadius = '8px';
  898. button.style.cursor = 'pointer';
  899. button.style.fontWeight = 'bold';
  900.  
  901. headerElement.insertBefore(button, headerElement.lastElementChild);
  902.  
  903. button.addEventListener('click', function () {
  904. window.open(
  905. 'https://docs.google.com/spreadsheets/d/13wUFhhssuPdAONI_OmRJi6l_Bs7KRZXDgVFCn7uJJNQ/edit?gid=706159343#gid=706159343',
  906. '_blank'
  907. );
  908. });
  909.  
  910. return button;
  911. }
  912.  
  913. // UNAVAILABLE CRIME create sheets button
  914. function createUnavailable() {
  915. // Don't just keep adding buttons!
  916. let existingButton = document.body.querySelector('.hf-sheets-button');
  917. if (existingButton) return;
  918.  
  919. let headerElement = document.body.querySelector('.crimes-app-header');
  920.  
  921. let button = document.createElement('button');
  922. button.textContent = 'Google Sheets';
  923. button.title = `Crime Profitability is (currently) unavailable for this crime`;
  924. button.classList.add('hf-sheets-button');
  925. button.style.background = 'var(--input-money-error-border-color)';
  926. button.style.color = 'var(--btn-color)';
  927. button.style.borderRadius = '8px';
  928. button.style.cursor = 'pointer';
  929. button.style.fontWeight = 'bold';
  930.  
  931. headerElement.insertBefore(button, headerElement.lastElementChild);
  932.  
  933. button.addEventListener('click', function () {
  934. window.open(
  935. 'https://docs.google.com/spreadsheets/d/13wUFhhssuPdAONI_OmRJi6l_Bs7KRZXDgVFCn7uJJNQ/edit?gid=0#gid=0',
  936. '_blank'
  937. );
  938. });
  939.  
  940. return button;
  941. }
  942.  
  943. // CRIME HUB display value per nerve
  944. function crimeHub(data, retries = 30) {
  945. let crimeHubRoot = document.body.querySelector('.crimes-hub-root');
  946. let crimes = crimeHubRoot?.querySelectorAll('.crimes-hub-crime');
  947.  
  948. if (!crimeHubRoot || !crimes || crimes.length < 2) {
  949. if (retries > 0) {
  950. setTimeout(() => crimeHub(data, retries - 1), 100);
  951. } else {
  952. console.warn('[HF] Gave up looking for the crime hub after 30 retries.');
  953. }
  954. return;
  955. }
  956.  
  957. for (let crime of crimes) {
  958. let titleElement = crime.querySelector('.crimeTitle___Q9cpR');
  959. let title = titleElement?.textContent.trim();
  960. if (title) {
  961. findMaximumProfit(data, title, crime);
  962. }
  963. }
  964. }
  965.  
  966. // HELPER FUNCTION for the CRIME HUB to find and display maximum profit per crime type
  967. function findMaximumProfit(data, crimeTitle, crimeElement) {
  968. if (!settings[crimeTitle]) return;
  969.  
  970. // Don't just keep adding stuff!
  971. let alreadyAdded = crimeElement.querySelector('.hf-value');
  972. if (alreadyAdded) return;
  973.  
  974. let maximum = -Infinity;
  975. for (let crimeData of data) {
  976. if (crimeData.crime.toLowerCase() !== crimeTitle.toLowerCase()) continue;
  977. let moneyPerNerve = parseInt(crimeData['$/N']?.replace(/[$,]/g, ''));
  978. if (moneyPerNerve > maximum) maximum = moneyPerNerve;
  979. }
  980.  
  981. let span = document.createElement('span');
  982. span.textContent = `${maximum < 0 ? '-' : ''}$${Math.abs(maximum).toLocaleString('en-US')} / N`;
  983. span.classList.add('hf-value');
  984. span.style.position = 'absolute';
  985. span.style.justifySelf = 'anchor-center';
  986. span.style.marginTop = '-38px';
  987. span.style.padding = '8px';
  988. span.style.borderRadius = '15px';
  989. span.style.background = 'var(--default-bg-19-gradient)';
  990. span.style.color = 'var(--default-base-white-color)';
  991.  
  992. if (maximum < threshold) span.style.background = 'var(--default-bg-18-gradient)';
  993. if (maximum < 0) span.style.background = 'var(--default-bg-17-gradient)';
  994. if (maximum === -Infinity) {
  995. span.textContent = '$/N N/A';
  996. span.style.background = 'var(--default-bg-18-gradient)';
  997. }
  998.  
  999. let titleBar = crimeElement.querySelector('.titleBar___O6ygy');
  1000. if (!titleBar) {
  1001. titleBar = crimeElement.querySelector('.titleAndStatus___q2yBJ');
  1002. let statusGroups = titleBar.querySelector('.statusGroups___EtLVR');
  1003. titleBar.insertBefore(span, statusGroups);
  1004. span.style.position = '';
  1005. span.style.justifySelf = '';
  1006. span.style.marginTop = '';
  1007. span.style.background = '';
  1008. span.style.color = 'var(--default-base-green-color)';
  1009. span.style.padding = '';
  1010. span.style.borderRadius = '';
  1011. span.style.marginBottom = '-3px';
  1012. if (maximum < threshold) span.style.color = 'var(--default-base-gold-color)';
  1013. if (maximum < 0) span.style.color = 'var(--default-base-important-color)';
  1014. if (maximum === -Infinity) span.style.color = 'var(--default-base-gold-color)';
  1015. } else {
  1016. titleBar.insertBefore(span, titleBar.firstChild);
  1017. }
  1018. }
  1019.  
  1020. // HELPER FUNCTION to convert a CSV string into an array of objects
  1021. function parseCSV(text) {
  1022. let rows = text.trim().split('\n');
  1023.  
  1024. let parseRow = (row) => {
  1025. let regex = /"(.*?)"(?:,|$)/g;
  1026. let result = [];
  1027. let match;
  1028.  
  1029. while ((match = regex.exec(row)) !== null) result.push(match[1]);
  1030.  
  1031. return result.slice(0, 8); // Only keep first 8!! columns
  1032. };
  1033.  
  1034. let headers = parseRow(rows.shift());
  1035.  
  1036. // Rename the first "7BFS" to "7BFS attempts"
  1037. let found = false;
  1038. headers = headers.map(header => {
  1039. if (header === '7BFS' && !found) {
  1040. found = true;
  1041. return '7BFS attempts';
  1042. }
  1043. return header;
  1044. });
  1045.  
  1046. return rows.map(row => {
  1047. let values = parseRow(row);
  1048. let entry = {};
  1049. headers.forEach((header, index) => {
  1050. entry[header] = values[index];
  1051. });
  1052. return entry;
  1053. });
  1054. }
  1055.  
  1056.  
  1057. // FETCH FUNCTION to fetch data from Emforus' sheet
  1058. function fetchData() {
  1059. let currentEpoch = Math.floor(Date.now() / 1000);
  1060. let twentyFourHours = 86400;
  1061.  
  1062. if (rememberedCrackingData && lastFetched && (currentEpoch - lastFetched) <= twentyFourHours) {
  1063. let data = rememberedData;
  1064.  
  1065. cachedData = data;
  1066.  
  1067. let crackingData = rememberedCrackingData;
  1068. cachedCrackingData = crackingData;
  1069.  
  1070. displayData(data, crackingData);
  1071.  
  1072. return;
  1073. }
  1074.  
  1075. let urls = {
  1076. all: emforusData,
  1077. cracking: crackingData
  1078. };
  1079.  
  1080. let results = {};
  1081. let completed = 0;
  1082. let total = Object.keys(urls).length;
  1083.  
  1084. Object.entries(urls).forEach(([type, url]) => {
  1085. GM[httpRequest]({
  1086. method: 'GET',
  1087. url: url,
  1088. responseType: 'text',
  1089. onload: function(response) {
  1090. const data = parseCSV(response.responseText);
  1091. results[type] = data;
  1092.  
  1093. if (type === 'all') {
  1094. cachedData = data;
  1095. localStorage.setItem('hf-crime-profitability-data', JSON.stringify(data));
  1096. } else if (type === 'cracking') {
  1097. cachedCrackingData = data;
  1098. localStorage.setItem('hf-crime-profitability-cracking-data', JSON.stringify(data));
  1099. }
  1100.  
  1101. completed++;
  1102. if (completed === total) {
  1103. let currentEpoch = Math.floor(Date.now() / 1000);
  1104. localStorage.setItem('hf-crime-profitability-last-fetched', currentEpoch);
  1105. displayData(results.all, results.cracking);
  1106. }
  1107. },
  1108. onerror: function(response) {
  1109. console.error(`Error fetching ${type} data:`, response);
  1110. }
  1111. });
  1112. });
  1113. }
  1114.  
  1115. // HELPER FUNCTION to create a mutation observer to watch for changes on the page
  1116. function createObserver(element, info, crackingInfo, disposalPage) {
  1117. let target;
  1118. target = element;
  1119.  
  1120. if (!target) {
  1121. console.error(`[HF] Mutation Observer target not found.`);
  1122. return;
  1123. }
  1124.  
  1125. let observer = new MutationObserver(function(mutationsList, observer) {
  1126. for (let mutation of mutationsList) {
  1127. if (mutation.type === 'childList') {
  1128. mutation.addedNodes.forEach(node => {
  1129. if (node?.classList?.contains('virtual-item')) {
  1130. if (!disposalPage) {
  1131. crimePage(info, crackingInfo, true);
  1132. } else {
  1133. disposal(info, true);
  1134. }
  1135. }
  1136. });
  1137.  
  1138. mutation.removedNodes.forEach(node => {
  1139. if (node?.classList?.contains('virtual-item')) {
  1140. if (!disposalPage) {
  1141. crimePage(info, crackingInfo, true);
  1142. } else {
  1143. disposal(info, true);
  1144. }
  1145. }
  1146. });
  1147. }
  1148. }
  1149. });
  1150.  
  1151. let config = { attributes: true, childList: true, subtree: true, characterData: true };
  1152. observer.observe(target, config);
  1153. }
  1154.  
  1155. // Attach click event listener
  1156. document.body.addEventListener('click', handleButtonClick);
  1157.  
  1158. function handleButtonClick(event) {
  1159. setTimeout(() => {
  1160. handlePageChange();
  1161. }, 500);
  1162. }
  1163.  
  1164. // Check if there's a page chance - if yes, rerun script
  1165. function handlePageChange() {
  1166. if (window.location.href === currentHref) return;
  1167.  
  1168. let existingButton = document.body.querySelector('.hf-sort-button');
  1169. if (existingButton) existingButton.remove();
  1170.  
  1171. displayData(cachedData, cachedCrackingData);
  1172.  
  1173. currentHref = window.location.href;
  1174. }
  1175.  
  1176. // Adds a settings button to the CRIME HUB page
  1177. function createSettingsButton() {
  1178. // Don't just keep adding buttons!
  1179. let existingButton = document.body.querySelector('.hf-sort-button');
  1180. if (existingButton) return;
  1181.  
  1182. let mobile = document.body.querySelector('.area-mobile___BH0Ku');
  1183.  
  1184. let headerElement = document.body.querySelector('.crimes-app-header');
  1185.  
  1186. if (!headerElement) headerElement = document.body.querySelector('.content-title');
  1187.  
  1188. let button = document.createElement('button');
  1189. button.textContent = 'Crime Profitability Settings';
  1190. button.classList.add('hf-sort-button');
  1191. button.style.background = 'var(--input-money-error-border-color)';
  1192. button.style.color = 'var(--btn-color)';
  1193. button.style.borderRadius = '8px';
  1194. button.style.cursor = 'pointer';
  1195. button.style.fontWeight = 'bold';
  1196.  
  1197. if (mobile) {
  1198. button.textContent = `$/N Settings`;
  1199. headerElement.style.flexWrap = 'wrap';
  1200. headerElement.style.height = 'fit-content';
  1201. headerElement.style.justifyContent = 'flex-end';
  1202. headerElement.style.paddingBottom = '10px';
  1203. headerElement.style.rowGap = '10px';
  1204. headerElement.style.columnGap = '20px';
  1205. }
  1206.  
  1207. headerElement.insertBefore(button, headerElement.children[1]);
  1208.  
  1209. button.addEventListener('click', function () {
  1210. createModal();
  1211. });
  1212.  
  1213. return button;
  1214. }
  1215.  
  1216. // Function to create the SETTINGS modal
  1217. function createModal() {
  1218. let mobile = document.body.querySelector('.area-mobile___BH0Ku');
  1219.  
  1220. let cachedSettings = settings;
  1221. let cachedThreshold = threshold;
  1222.  
  1223. let modal = document.createElement('div');
  1224. modal.style.position = 'fixed';
  1225. modal.style.top = '50%';
  1226. modal.style.left = '50%';
  1227. modal.style.transform = 'translate(-50%, -50%)';
  1228. modal.style.padding = '20px';
  1229. modal.style.backgroundColor = 'var(--sidebar-area-bg-warning-active)';
  1230. modal.style.border = '2px solid var(--default-tabs-color)';
  1231. modal.style.borderRadius = '15px';
  1232. modal.style.maxWidth = '400px';
  1233. modal.style.zIndex = '9999';
  1234. modal.style.maxHeight = '60vh';
  1235. modal.style.overflow = 'hidden';
  1236. modal.style.display = 'flex';
  1237. modal.style.flexDirection = 'column';
  1238.  
  1239. let titleContainer = document.createElement('div');
  1240. titleContainer.textContent = 'Crime Profitability Settings';
  1241. titleContainer.style.fontSize = 'x-large';
  1242. titleContainer.style.fontWeight = 'bolder';
  1243. titleContainer.style.paddingBottom = '8px';
  1244.  
  1245. let scrollContainer = document.createElement('div');
  1246. scrollContainer.style.maxHeight = '100%';
  1247. scrollContainer.style.flex = '1'; // Fill remaining space
  1248. scrollContainer.style.overflowY = 'auto';
  1249. scrollContainer.style.marginTop = '10px';
  1250.  
  1251. let mainContainer = document.createElement('div');
  1252. mainContainer.style.display = 'flex';
  1253. mainContainer.style.flexDirection = 'column';
  1254. scrollContainer.appendChild(mainContainer);
  1255.  
  1256. let creditSpan = document.createElement('span');
  1257. 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>.`;
  1258. creditSpan.style.paddingBottom = '8px';
  1259. mainContainer.appendChild(creditSpan);
  1260.  
  1261. let checkboxDiv = document.createElement('div');
  1262. checkboxDiv.style.display = 'flex';
  1263. checkboxDiv.style.flexDirection = 'column';
  1264. checkboxDiv.style.paddingTop = '15px';
  1265.  
  1266. let infoSpan = document.createElement('span');
  1267. infoSpan.textContent = 'Enable/disable which crimes you want crime profitability to appear for here.';
  1268. checkboxDiv.appendChild(infoSpan);
  1269.  
  1270. if (!mobile) {
  1271. checkboxDiv.style.flexDirection = 'row';
  1272. checkboxDiv.style.flexWrap = 'wrap';
  1273.  
  1274. infoSpan.style.flex = '1 1 100%';
  1275.  
  1276. let leftColumn = document.createElement('div');
  1277. leftColumn.style.display = 'flex';
  1278. leftColumn.style.flexDirection = 'column';
  1279.  
  1280. let middleColumn = document.createElement('div');
  1281. middleColumn.style.display = 'flex';
  1282. middleColumn.style.flexDirection = 'column';
  1283. middleColumn.style.paddingLeft = '30px';
  1284.  
  1285. let rightColumn = document.createElement('div');
  1286. rightColumn.style.display = 'flex';
  1287. rightColumn.style.flexDirection = 'column';
  1288. rightColumn.style.paddingLeft = '30px';
  1289.  
  1290. let crimes = ['Overview', 'Search for Cash', 'Bootlegging', 'Graffiti', 'Shoplifting', 'Pickpocketing',
  1291. 'Card Skimming', 'Burglary', 'Hustling', 'Disposal', 'Cracking', 'Forgery', 'Scamming'];
  1292.  
  1293. let enableAll = addToggle(checkboxDiv, 'Enable All');
  1294. enableAll.style.flex = '1 1 100%';
  1295.  
  1296. checkboxDiv.appendChild(leftColumn);
  1297. checkboxDiv.appendChild(middleColumn);
  1298. checkboxDiv.appendChild(rightColumn);
  1299.  
  1300. crimes.forEach((crime, index) => {
  1301. const parent = [leftColumn, middleColumn, rightColumn][index % 3];
  1302. addToggle(parent, crime);
  1303. });
  1304. } else {
  1305. modal.style.minWidth = '60vw';
  1306. titleContainer.style.fontSize = 'large';
  1307. titleContainer.textContent = '$/N Settings';
  1308.  
  1309. let crimes = ['Enable All', 'Overview', 'Search for Cash', 'Bootlegging', 'Graffiti', 'Shoplifting', 'Pickpocketing',
  1310. 'Card Skimming', 'Burglary', 'Hustling', 'Disposal', 'Cracking', 'Forgery', 'Scamming'];
  1311.  
  1312. for (let crime of crimes) {
  1313. addToggle(checkboxDiv, crime);
  1314. }
  1315. }
  1316.  
  1317. createToggleStyleSheet();
  1318.  
  1319. let warningSpan = document.createElement('span');
  1320. warningSpan.textContent = `* Currently unavailable`;
  1321. warningSpan.style.paddingTop = '5px';
  1322. checkboxDiv.appendChild(warningSpan);
  1323.  
  1324. mainContainer.appendChild(checkboxDiv);
  1325.  
  1326. let inputContainer = document.createElement('div');
  1327. inputContainer.style.paddingTop = '20px';
  1328. inputContainer.style.display = 'flex';
  1329. inputContainer.style.flexDirection = 'column';
  1330.  
  1331. let inputTitle = document.createElement('span');
  1332. inputTitle.textContent = '$/N Threshold';
  1333. inputTitle.style.fontWeight = 'bold';
  1334. inputTitle.style.fontSize = 'larger';
  1335. inputTitle.style.paddingBottom = '5px';
  1336. inputContainer.appendChild(inputTitle);
  1337.  
  1338. let inputExplanation = document.createElement('span');
  1339. inputExplanation.textContent = `You're able to set a preferred minimum value per nerve in the following input.
  1340. 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".
  1341. If no threshold is filled in, it will be $0 by default.`;
  1342. inputExplanation.style.paddingBottom = '8px';
  1343. inputContainer.appendChild(inputExplanation);
  1344.  
  1345. let inputLabelContainer = document.createElement('div');
  1346. inputContainer.appendChild(inputLabelContainer);
  1347.  
  1348. let inputLabel = document.createElement('span');
  1349. inputLabel.textContent = 'Minimum $/N threshold:';
  1350. inputLabel.style.paddingRight = '5px';
  1351. inputLabelContainer.appendChild(inputLabel);
  1352.  
  1353. createTextNumberInput(inputLabelContainer);
  1354.  
  1355. mainContainer.appendChild(inputContainer);
  1356.  
  1357. // Create a container for "Done" and "Cancel" buttons
  1358. let buttonContainer = document.createElement('div');
  1359. buttonContainer.style.display = 'flex';
  1360. buttonContainer.style.justifyContent = 'space-between';
  1361. buttonContainer.style.paddingTop = '15px';
  1362.  
  1363. let cancelButton = document.createElement('button');
  1364. cancelButton.textContent = 'Cancel';
  1365. cancelButton.style.color = 'black';
  1366. cancelButton.style.border = '1px solid black';
  1367. cancelButton.style.borderRadius = '5px';
  1368. cancelButton.style.backgroundColor = '#ccc';
  1369. cancelButton.addEventListener('click', function () {
  1370. settings = cachedSettings;
  1371. threshold = cachedThreshold;
  1372.  
  1373. localStorage.setItem('hf-crime-profitability-settings', JSON.stringify(settings));
  1374. localStorage.setItem('hf-crime-profitability-threshold', threshold);
  1375.  
  1376. modal.style.display = 'none';
  1377. });
  1378. buttonContainer.appendChild(cancelButton);
  1379.  
  1380. // Add style for hover effect
  1381. cancelButton.style.cursor = 'pointer';
  1382.  
  1383. // Add event listeners for hover effect
  1384. cancelButton.addEventListener('mouseover', function () {
  1385. cancelButton.style.fontWeight = 'bold';
  1386. });
  1387.  
  1388. cancelButton.addEventListener('mouseout', function () {
  1389. cancelButton.style.fontWeight = '';
  1390. });
  1391.  
  1392. let doneButton = document.createElement('button');
  1393. doneButton.textContent = 'Done';
  1394. doneButton.style.color = 'black';
  1395. doneButton.style.border = '1px solid black';
  1396. doneButton.style.borderRadius = '5px';
  1397. doneButton.style.backgroundColor = '#ccc';
  1398. doneButton.addEventListener('click', function () {
  1399. location.reload();
  1400. modal.style.display = 'none';
  1401. });
  1402. buttonContainer.appendChild(doneButton);
  1403.  
  1404. // Add style for hover effect
  1405. doneButton.style.cursor = 'pointer';
  1406.  
  1407. // Add event listeners for hover effect
  1408. doneButton.addEventListener('mouseover', function () {
  1409. doneButton.style.fontWeight = 'bold';
  1410. });
  1411.  
  1412. doneButton.addEventListener('mouseout', function () {
  1413. doneButton.style.fontWeight = '';
  1414. });
  1415.  
  1416. modal.appendChild(titleContainer);
  1417. modal.appendChild(scrollContainer);
  1418. modal.appendChild(buttonContainer);
  1419.  
  1420. document.body.appendChild(modal);
  1421.  
  1422. return modal;
  1423. }
  1424.  
  1425. // Add the checkbox toggle in the SETTINGS modal
  1426. function addToggle(container, labelText) {
  1427. let toggleContainer = document.createElement('div');
  1428. toggleContainer.classList.add('hf-toggle-container');
  1429. toggleContainer.style.paddingTop = '5px';
  1430.  
  1431. let text = document.createElement('span');
  1432. text.textContent = labelText;
  1433. text.style.paddingLeft = '5px';
  1434.  
  1435. let label = document.createElement('label');
  1436. label.classList.add('switch');
  1437.  
  1438. let input = document.createElement('input');
  1439. input.type = 'checkbox';
  1440. input.classList.add('hf-checkbox');
  1441.  
  1442. input.checked = true; // Check on by default
  1443.  
  1444. let span = document.createElement('span');
  1445. span.classList.add('slider', 'round');
  1446.  
  1447. if (labelText === 'Graffiti' || labelText === 'Card Skimming' || labelText === 'Hustling' || labelText === 'Scamming') {
  1448. text.textContent += '*';
  1449. input.checked = false;
  1450. }
  1451.  
  1452. let savedInfo = settings[labelText];
  1453.  
  1454. if (savedInfo === true) input.checked = true;
  1455. if (savedInfo === false) input.checked = false;
  1456.  
  1457. if (labelText === 'Enable All') input.checked = false;
  1458.  
  1459. settings[labelText] = input.checked;
  1460. localStorage.setItem('hf-crime-profitability-settings', JSON.stringify(settings));
  1461.  
  1462. toggleContainer.appendChild(label);
  1463. toggleContainer.appendChild(text);
  1464. label.appendChild(input);
  1465. label.appendChild(span);
  1466. container.appendChild(toggleContainer);
  1467.  
  1468. // Add event listener to detect changes in the checkbox state
  1469. input.addEventListener('change', function() {
  1470. if (input.checked) {
  1471. if (labelText === 'Enable All') {
  1472. let inputs = document.body.querySelectorAll('.hf-checkbox');
  1473. for (let input of inputs) {
  1474. input.checked = true;
  1475. let event = new Event('change', { bubbles: true });
  1476. input.dispatchEvent(event);
  1477. }
  1478.  
  1479. text.textContent = 'Disable All';
  1480. return;
  1481. }
  1482.  
  1483. settings[labelText] = true;
  1484. localStorage.setItem('hf-crime-profitability-settings', JSON.stringify(settings));
  1485. } else {
  1486. if (labelText === 'Enable All' || labelText === 'Disable All') {
  1487. let inputs = document.body.querySelectorAll('.hf-checkbox');
  1488. for (let input of inputs) {
  1489. input.checked = false;
  1490. let event = new Event('change', { bubbles: true });
  1491. input.dispatchEvent(event);
  1492. }
  1493.  
  1494. text.textContent = 'Enable All';
  1495. return;
  1496. }
  1497.  
  1498. settings[labelText] = false;
  1499. localStorage.setItem('hf-crime-profitability-settings', JSON.stringify(settings));
  1500. }
  1501. });
  1502.  
  1503. return toggleContainer;
  1504. }
  1505.  
  1506. // HELPER FUNCTION to create a "Number" input
  1507. function createTextNumberInput(element) {
  1508. let input = document.createElement('input');
  1509. input.className = 'hf-number-input';
  1510. input.type = 'text'; // Allow '5K', '1.2M', etc.
  1511. input.value = threshold.toLocaleString('en-US') || '';
  1512. input.style.padding = '5px';
  1513. input.style.borderRadius = '5px';
  1514. input.style.border = '1px solid black';
  1515. input.style.background = '#ccc';
  1516. input.style.width = '70px';
  1517.  
  1518. element.appendChild(input);
  1519.  
  1520. input.addEventListener('keydown', function (e) {
  1521. let allowedKeys = [
  1522. 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab',
  1523. '.', 'k', 'K', 'm', 'M', 'b', 'B', ','
  1524. ];
  1525.  
  1526. let isDigit = e.key >= '0' && e.key <= '9';
  1527. if (!isDigit && !allowedKeys.includes(e.key)) {
  1528. e.preventDefault();
  1529. }
  1530. });
  1531.  
  1532. input.addEventListener('input', function () {
  1533. let rawValue = input.value.trim().toUpperCase();
  1534. let parsedValue = parseShorthandNumber(rawValue);
  1535.  
  1536. if (!isNaN(parsedValue)) {
  1537. input.value = parsedValue.toLocaleString('en-US');
  1538.  
  1539. threshold = parsedValue;
  1540. localStorage.setItem('hf-crime-profitability-threshold', parsedValue);
  1541. }
  1542. });
  1543. }
  1544.  
  1545. // HELPER FUNCTION to parse shorthand numbers
  1546. function parseShorthandNumber(str) {
  1547. str = str.replace(/,/g, ''); // Remove commas
  1548.  
  1549. if (/^\d+(\.\d+)?$/.test(str)) {
  1550. return parseFloat(str);
  1551. }
  1552.  
  1553. let match = str.match(/^(\d+(\.\d+)?)([KMB])$/);
  1554. if (!match) return NaN;
  1555.  
  1556. let number = parseFloat(match[1]);
  1557. let suffix = match[3];
  1558.  
  1559. switch (suffix) {
  1560. case 'K': return number * 1_000;
  1561. case 'M': return number * 1_000_000;
  1562. case 'B': return number * 1_000_000_000;
  1563. default: return NaN;
  1564. }
  1565. }
  1566.  
  1567.  
  1568. // HELPER FUNCTION to create a style sheet to make the fancier toggles work
  1569. function createToggleStyleSheet() {
  1570. let styles = `
  1571. .switch {
  1572. position: relative;
  1573. display: inline-block;
  1574. width: 20px;
  1575. height: 10px;
  1576. top: 1px;
  1577. }
  1578. .switch input {
  1579. opacity: 0;
  1580. width: 0;
  1581. height: 0;
  1582. }
  1583. .slider {
  1584. position: absolute;
  1585. cursor: pointer;
  1586. top: 0;
  1587. left: 0;
  1588. right: 0;
  1589. bottom: 0;
  1590. background-color: #ccc;
  1591. transition: .4s;
  1592. }
  1593. .slider:before {
  1594. position: absolute;
  1595. content: "";
  1596. height: 10px;
  1597. width: 10px;
  1598. background-color: white;
  1599. transition: .4s;
  1600. }
  1601. input:checked + .slider {
  1602. background-color: #2196F3;
  1603. }
  1604. input:focus + .slider {
  1605. box-shadow: 0 0 1px #2196F3;
  1606. }
  1607. input:checked + .slider:before {
  1608. transform: translateX(10px);
  1609. }
  1610. .slider.round {
  1611. border-radius: 34px;
  1612. }
  1613. .slider.round:before {
  1614. border-radius: 50%;
  1615. }
  1616. `;
  1617.  
  1618. // Add the styles to a <style> tag in the document head
  1619. let styleSheet = document.createElement("style");
  1620. styleSheet.type = "text/css";
  1621. styleSheet.innerText = styles;
  1622. document.head.appendChild(styleSheet);
  1623. }
  1624.  
  1625. // MAIN FUNCTION TO DISPLAY EVERYTHING BASED ON HREF
  1626. function displayData(data, crackingData) {
  1627. let href = window.location.href;
  1628. let currentHref = href;
  1629.  
  1630. let crimes = ['Overview', 'Search for Cash', 'Bootlegging', 'Graffiti', 'Shoplifting', 'Pickpocketing',
  1631. 'Card Skimming', 'Burglary', 'Hustling', 'Disposal', 'Cracking', 'Forgery', 'Scamming'];
  1632.  
  1633. if (!settings || Object.keys(settings).length === 0) {
  1634. settings = {};
  1635.  
  1636. for (let crime of crimes) {
  1637. settings[crime] = true;
  1638. if (crime === 'Graffiti' || crime === 'Card Skimming' || crime === 'Burglary' || crime === 'Hustling' || crime === 'Scamming') settings[crime] = false;
  1639. }
  1640. }
  1641.  
  1642. if (href.includes('searchforcash')) {
  1643. if (settings['Search for Cash']) crimePage(data);
  1644. } else if (href.includes('bootlegging')) {
  1645. if (settings.Bootlegging) bootlegging(data);
  1646. } else if (href.includes('graffiti')) {
  1647. if (settings.Graffiti) createUnavailable();
  1648. } else if (href.includes('shoplifting')) {
  1649. if (settings.Shoplifting) crimePage(data);
  1650. } else if (href.includes('pickpocketing')) {
  1651. if (settings.Pickpocketing) crimePage(data);
  1652. } else if (href.includes ('cardskimming')) {
  1653. // Cannot be calculated as per Emforus
  1654. if (settings['Card Skimming']) createUnavailable();
  1655. } else if (href.includes('burglary')) {
  1656. if (settings.Burglary) crimePage(data);
  1657. } else if (href.includes('hustling')) {
  1658. // Cannot be calculated as per Emforus
  1659. if (settings.Hustling) createUnavailable();
  1660. } else if (href.includes('disposal')) {
  1661. if (settings.Disposal) disposal(data);
  1662. } else if (href.includes('cracking')) {
  1663. if (settings.Cracking) crimePage(data, crackingData);
  1664. } else if (href.includes('forgery')) {
  1665. if (settings.Forgery) crimePage(data);
  1666. } else if (href.includes('scamming')) {
  1667. // Cannot be calculated as per Emforus
  1668. if (settings.Scamming) createUnavailable();
  1669. } else {
  1670. setTimeout(() => createSettingsButton(), 500);
  1671. if (settings.Overview) crimeHub(data);
  1672. }
  1673. }
  1674.  
  1675. fetchData();
  1676.  
  1677. // Checking arrows and document click handler only work like half of the time, so interval
  1678. setInterval(handlePageChange, 200);
  1679.  
  1680.  
  1681. })();