Crime Morale

A comprehensive tool for Crime 2.0

  1. // ==UserScript==
  2. // @name Crime Morale
  3. // @namespace https://github.com/tobytorn
  4. // @description A comprehensive tool for Crime 2.0
  5. // @author tobytorn [1617955]
  6. // @match https://www.torn.com/loader.php?sid=crimes*
  7. // @version 1.4.10
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @grant unsafeWindow
  11. // @run-at document-start
  12. // @supportURL https://github.com/tobytorn/crime-morale
  13. // @license MIT
  14. // @require https://unpkg.com/jquery@3.7.0/dist/jquery.min.js
  15. // ==/UserScript==
  16.  
  17. (function () {
  18. 'use strict';
  19.  
  20. // Avoid duplicate injection in TornPDA
  21. if (window.CRIME_MORALE_INJECTED) {
  22. return;
  23. }
  24. window.CRIME_MORALE_INJECTED = true;
  25. console.log('Userscript Crime Morale starts');
  26.  
  27. const LOCAL_STORAGE_PREFIX = 'CRIME_MORALE_';
  28. const STORAGE_MORALE = 'morale';
  29. const STYLE_ELEMENT_ID = 'CRIME-MORALE-STYLE';
  30.  
  31. function getLocalStorage(key, defaultValue) {
  32. const value = window.localStorage.getItem(LOCAL_STORAGE_PREFIX + key);
  33. try {
  34. return JSON.parse(value) ?? defaultValue;
  35. } catch (err) {
  36. return defaultValue;
  37. }
  38. }
  39.  
  40. function setLocalStorage(key, value) {
  41. window.localStorage.setItem(LOCAL_STORAGE_PREFIX + key, JSON.stringify(value));
  42. }
  43.  
  44. const isPda = window.GM_info?.scriptHandler?.toLowerCase().includes('tornpda');
  45. const [getValue, setValue] =
  46. isPda || typeof window.GM_getValue !== 'function' || typeof window.GM_setValue !== 'function'
  47. ? [getLocalStorage, setLocalStorage]
  48. : [window.GM_getValue, window.GM_setValue];
  49.  
  50. function addStyle(css) {
  51. const style =
  52. document.getElementById(STYLE_ELEMENT_ID) ??
  53. (function () {
  54. const style = document.createElement('style');
  55. style.id = STYLE_ELEMENT_ID;
  56. document.head.appendChild(style);
  57. return style;
  58. })();
  59. style.appendChild(document.createTextNode(css));
  60. }
  61.  
  62. function formatLifetime(seconds) {
  63. const hours = Math.floor(seconds / 3600);
  64. const text =
  65. hours >= 72
  66. ? `${Math.floor(hours / 24)}d`
  67. : hours > 0
  68. ? `${hours}h`
  69. : seconds >= 0
  70. ? `${Math.floor(seconds / 60)}m`
  71. : '';
  72. const color = hours >= 24 ? 't-gray-c' : hours >= 12 ? 't-yellow' : hours >= 0 ? 't-red' : '';
  73. return { seconds, hours, text, color };
  74. }
  75.  
  76. async function checkDemoralization(data) {
  77. const demMod = (data.DB || {}).demMod;
  78. if (typeof demMod !== 'number') {
  79. return;
  80. }
  81. const morale = 100 - demMod;
  82. updateMorale(morale);
  83. await setValue(STORAGE_MORALE, morale);
  84. }
  85.  
  86. class BurglaryObserver {
  87. constructor() {
  88. this.data = getValue('burglary', {});
  89. this.data.favorite = this.data.favorite ?? [];
  90. this.properties = null;
  91. this.crimeOptions = null;
  92. this.observer = new MutationObserver((mutations) => {
  93. const isAdd = mutations.some((mutation) => {
  94. for (const added of mutation.addedNodes) {
  95. if (added instanceof HTMLElement) {
  96. return true;
  97. }
  98. }
  99. return false;
  100. });
  101. if (!isAdd) {
  102. return;
  103. }
  104. for (const element of this.crimeOptions) {
  105. if (!element.classList.contains('cm-bg-seen')) {
  106. element.classList.add('cm-bg-seen');
  107. this._refreshCrimeOption(element);
  108. }
  109. }
  110. });
  111. }
  112.  
  113. start() {
  114. if (this.crimeOptions) {
  115. return;
  116. }
  117. this.crimeOptions = document.body.getElementsByClassName('crime-option');
  118. this.observer.observe($('.burglary-root')[0], { subtree: true, childList: true });
  119. }
  120.  
  121. stop() {
  122. this.crimeOptions = null;
  123. this.observer.disconnect();
  124. }
  125.  
  126. onNewData(data) {
  127. this.start();
  128. this.properties = data.DB?.crimesByType?.properties;
  129. this._refreshCrimeOptions();
  130. }
  131.  
  132. _refreshCrimeOptions() {
  133. for (const element of this.crimeOptions) {
  134. this._refreshCrimeOption(element);
  135. }
  136. }
  137.  
  138. _refreshCrimeOption(element) {
  139. if (!this.properties) {
  140. return;
  141. }
  142. const $element = $(element);
  143. const $title = $element.find('[class*=crimeOptionSection___]').first();
  144. $title.find('.cm-bg-lifetime').remove();
  145. const guessedProperty = this._guessCrimeOptionData($element);
  146. const property = this._checkCrimeOptionData($element, guessedProperty);
  147. if (!property) {
  148. $element.removeAttr('data-cm-id');
  149. return;
  150. }
  151. $element.attr('data-cm-id', property.subID);
  152. const now = Math.floor(Date.now() / 1000);
  153. const lifetime = formatLifetime(property.expire - now);
  154. if (lifetime.hours >= 0) {
  155. $title.css('position', 'relative');
  156. $title.append(`<div class="cm-bg-lifetime ${lifetime.color}">${lifetime.text}</div>`);
  157. }
  158. $element.find('.cm-bg-favor').remove();
  159. const $favor = $('<div class="cm-bg-favor"></div>');
  160. $favor.toggleClass('cm-bg-active', this.data.favorite.includes(property.title));
  161. $element.find('.crime-image').append($favor);
  162. $favor.on('click', () => {
  163. this._toggleFavorite(property.title);
  164. this._refreshCrimeOptions();
  165. });
  166. }
  167.  
  168. _guessCrimeOptionData($crimeOption) {
  169. const savedId = $crimeOption.attr('data-cm-id');
  170. if (savedId) {
  171. return this.properties.find((x) => x.subID === savedId);
  172. }
  173. const $item = $crimeOption.closest('.virtual-item');
  174. if ($item.prev().hasClass('lastOfGroup___YNUeQ')) {
  175. return this.properties[0];
  176. }
  177. let prevId = undefined;
  178. $item.prevAll().each(function () {
  179. prevId = $(this).find('.crime-option[data-cm-id]').attr('data-cm-id');
  180. if (prevId) {
  181. return false; // break the loop
  182. }
  183. });
  184. const prevIndex = this.properties.findIndex((x) => prevId && x.subID === prevId);
  185. if (prevIndex >= 0) {
  186. // Since we always scan crime options in document order,
  187. // $prevItemWithId and $item should correspond to adjacent data entries.
  188. return this.properties[prevIndex + 1];
  189. }
  190. if ($item.index() === 0) {
  191. const $nextOptionWithId = $item.nextAll().find('.crime-option[data-cm-id]').first();
  192. const nextId = $nextOptionWithId.attr('data-cm-id');
  193. const nextIndex = this.properties.findIndex((x) => x.subID && x.subID === nextId);
  194. const nextPos = $nextOptionWithId.closest('.virtual-item').index();
  195. if (nextIndex >= 0 && nextPos >= 0) {
  196. return this.properties[nextIndex - nextPos];
  197. }
  198. }
  199. return undefined;
  200. }
  201.  
  202. _checkCrimeOptionData($crimeOption, property) {
  203. if (property === undefined) {
  204. return undefined;
  205. }
  206. const { title, titleType } = this._getCrimeOptionTitle($crimeOption);
  207. return titleType && property[titleType] === title ? property : undefined;
  208. }
  209.  
  210. _getCrimeOptionTitle($crimeOption) {
  211. const mobileTitle = $crimeOption.find('.title___kOWyb').text();
  212. if (mobileTitle !== '') {
  213. return { title: mobileTitle, titleType: 'mobileTitle' };
  214. }
  215. const textNode = $crimeOption.find('.crimeOptionSection___hslpu')[0]?.firstChild;
  216. if (textNode?.nodeType === Node.TEXT_NODE) {
  217. return { title: textNode.textContent, titleType: 'title' };
  218. }
  219. return { title: null, titleType: null };
  220. }
  221.  
  222. _toggleFavorite(title) {
  223. const index = this.data.favorite.indexOf(title);
  224. if (index >= 0) {
  225. this.data.favorite.splice(index, 1);
  226. } else {
  227. this.data.favorite.push(title);
  228. }
  229. setValue('burglary', this.data);
  230. }
  231. }
  232. const burglaryObserver = new BurglaryObserver();
  233.  
  234. async function checkBurglary(crimeType, data) {
  235. if (crimeType !== '7') {
  236. burglaryObserver.stop();
  237. return;
  238. }
  239. burglaryObserver.onNewData(data);
  240. }
  241.  
  242. const PP_CYCLING = 0;
  243. const PP_DISTRACTED = 34; // eslint-disable-line no-unused-vars
  244. const PP_MUSIC = 102;
  245. const PP_LOITERING = 136;
  246. const PP_PHONE = 170;
  247. const PP_RUNNING = 204;
  248. const PP_SOLICITING = 238; // eslint-disable-line no-unused-vars
  249. const PP_STUMBLING = 272;
  250. const PP_WALKING = 306;
  251. const PP_BEGGING = 340;
  252.  
  253. const PP_SKINNY = 'Skinny';
  254. const PP_AVERAGE = 'Average';
  255. const PP_ATHLETIC = 'Athletic';
  256. const PP_MUSCULAR = 'Muscular';
  257. const PP_HEAVYSET = 'Heavyset';
  258. const PP_ANY_BUILD = [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC, PP_MUSCULAR, PP_HEAVYSET];
  259.  
  260. const PP_MARKS = {
  261. 'Drunk Man': { level: 1, status: [PP_STUMBLING], build: PP_ANY_BUILD },
  262. 'Drunk Woman': { level: 1, status: [PP_STUMBLING], build: PP_ANY_BUILD },
  263. 'Homeless Person': { level: 1, status: [PP_BEGGING], build: [PP_AVERAGE] },
  264. Junkie: { level: 1, status: [PP_STUMBLING], build: PP_ANY_BUILD },
  265. 'Elderly Man': { level: 1, status: [PP_WALKING], build: [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC, PP_HEAVYSET] },
  266. 'Elderly Woman': { level: 1, status: [PP_WALKING], build: [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC, PP_HEAVYSET] },
  267.  
  268. 'Young Man': { level: 2, status: [PP_MUSIC], build: [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC] },
  269. 'Young Woman': { level: 2, status: [PP_PHONE], build: [PP_SKINNY, PP_AVERAGE, PP_HEAVYSET] },
  270. Student: { level: 2, status: [PP_PHONE], build: [PP_SKINNY, PP_AVERAGE] },
  271. 'Classy Lady': {
  272. level: 2,
  273. status: [PP_PHONE, PP_WALKING],
  274. build: [PP_SKINNY, PP_HEAVYSET],
  275. bestBuild: [PP_HEAVYSET],
  276. },
  277. Laborer: { level: 2, status: [PP_PHONE], build: PP_ANY_BUILD },
  278. 'Postal Worker': { level: 2, status: [PP_WALKING], build: [PP_AVERAGE] },
  279.  
  280. 'Rich Kid': {
  281. level: 3,
  282. status: [PP_WALKING, PP_PHONE],
  283. build: [PP_SKINNY, PP_ATHLETIC, PP_HEAVYSET],
  284. bestBuild: [PP_ATHLETIC],
  285. },
  286. 'Sex Worker': { level: 3, status: [PP_PHONE], build: [PP_SKINNY, PP_AVERAGE], bestBuild: [PP_AVERAGE] },
  287. Thug: { level: 3, status: [PP_RUNNING], build: [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC], bestBuild: [PP_SKINNY] },
  288.  
  289. Businessman: {
  290. level: 4,
  291. status: [PP_PHONE],
  292. build: [PP_AVERAGE, PP_MUSCULAR, PP_HEAVYSET],
  293. bestBuild: [PP_MUSCULAR, PP_HEAVYSET],
  294. },
  295. Businesswoman: {
  296. level: 4,
  297. status: [PP_PHONE],
  298. build: [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC],
  299. bestBuild: [PP_ATHLETIC],
  300. },
  301. 'Gang Member': {
  302. level: 4,
  303. status: [PP_LOITERING],
  304. build: [PP_AVERAGE, PP_ATHLETIC, PP_MUSCULAR],
  305. bestBuild: [PP_AVERAGE],
  306. },
  307. Jogger: { level: 4, status: [PP_WALKING], build: [PP_ATHLETIC, PP_MUSCULAR], bestBuild: [PP_MUSCULAR] },
  308. Mobster: { level: 4, status: [PP_WALKING], build: [PP_SKINNY] },
  309.  
  310. Cyclist: { level: 5, status: [PP_CYCLING], build: ['1.52 m', `5'0"`, '1.62 m', `5'4"`] },
  311. 'Police Officer': {
  312. level: 6,
  313. status: [PP_RUNNING],
  314. build: PP_ANY_BUILD,
  315. bestBuild: [PP_SKINNY, '1.52 m', `5'0"`, '1.62 m', `5'4"`],
  316. },
  317. };
  318. let pickpocketingOb = null;
  319. let pickpocketingExitOb = null;
  320. let pickpocketingInterval = 0;
  321.  
  322. async function checkPickpocketing(crimeType) {
  323. if (crimeType !== '5') {
  324. stopPickpocketing();
  325. return;
  326. }
  327. const $wrapper = $('.pickpocketing-root');
  328. if ($wrapper.length === 0) {
  329. if (pickpocketingInterval === 0) {
  330. // This is the first fetch.
  331. pickpocketingInterval = setInterval(() => {
  332. const $wrapperInInterval = $('.pickpocketing-root');
  333. if ($wrapperInInterval.length === 0) {
  334. return;
  335. }
  336. clearInterval(pickpocketingInterval);
  337. pickpocketingInterval = 0;
  338. startPickpocketing($wrapperInInterval);
  339. }, 1000);
  340. }
  341. } else {
  342. startPickpocketing($wrapper);
  343. }
  344. }
  345.  
  346. function refreshPickpocketing() {
  347. const $wrapper = $('.pickpocketing-root');
  348. const now = Date.now();
  349. // Releasing reference to removed elements to avoid memory leak
  350. pickpocketingExitOb.disconnect();
  351. let isBelowExiting = false;
  352. $wrapper.find('.crime-option').each(function () {
  353. const $this = $(this);
  354. const top = Math.floor($this.position().top);
  355. const oldTop = parseInt($this.attr('data-cm-top'));
  356. if (top !== oldTop) {
  357. $this.attr('data-cm-top', top.toString());
  358. $this.attr('data-cm-timestamp', now.toString());
  359. }
  360. const timestamp = parseInt($this.attr('data-cm-timestamp')) || now;
  361. const isLocked = $this.is('[class*=locked___]');
  362. const isExiting = $this.is('[class*=exitActive___]');
  363. const isRecentlyMoved = now - timestamp <= 1000;
  364. $this
  365. .find('[class*=commitButtonSection___]')
  366. .toggleClass('cm-overlay', !isLocked && (isBelowExiting || isRecentlyMoved))
  367. .toggleClass('cm-overlay-fade', !isLocked && !isBelowExiting && isRecentlyMoved);
  368. isBelowExiting = isBelowExiting || isExiting;
  369.  
  370. if (!$this.is('[class*=cm-pp-level-]')) {
  371. const markAndTime = $this.find('[class*=titleAndProps___] > *:first-child').text().trim().toLowerCase();
  372. const iconPosStr = $this.find('[class*=timerCircle___] [class*=icon___]').css('background-position-y');
  373. const iconPosMatch = iconPosStr?.match(/(-?\d+)px/);
  374. const iconPos = -parseInt(iconPosMatch?.[1] ?? '');
  375. const build = $this.find('[class*=physicalPropsButton___]').text().trim().toLowerCase();
  376. for (const [mark, markInfo] of Object.entries(PP_MARKS)) {
  377. if (markAndTime.startsWith(mark.toLowerCase())) {
  378. if (markInfo.status.includes(iconPos) && markInfo.build.some((b) => build.includes(b.toLowerCase()))) {
  379. $this.addClass(`cm-pp-level-${markInfo.level}`);
  380. if (markInfo.bestBuild?.some((b) => build.includes(b.toLowerCase()))) {
  381. $this.addClass(`cm-pp-best-build`);
  382. }
  383. }
  384. break;
  385. }
  386. }
  387. }
  388.  
  389. pickpocketingExitOb.observe(this, { attributes: true, attributeFilter: ['class'], attributeOldValue: true });
  390. });
  391. }
  392.  
  393. function startPickpocketing($wrapper) {
  394. if (!pickpocketingOb) {
  395. pickpocketingOb = new MutationObserver(refreshPickpocketing);
  396. pickpocketingExitOb = new MutationObserver(function (mutations) {
  397. for (const mutation of mutations) {
  398. if (
  399. mutation.oldValue.indexOf('exitActive___') < 0 &&
  400. mutation.target.className.indexOf('exitActive___') >= 0
  401. ) {
  402. refreshPickpocketing();
  403. return;
  404. }
  405. }
  406. });
  407. }
  408. pickpocketingOb.observe($wrapper[0], {
  409. childList: true,
  410. characterData: true,
  411. subtree: true,
  412. });
  413. }
  414.  
  415. function stopPickpocketing() {
  416. if (!pickpocketingOb) {
  417. return;
  418. }
  419. pickpocketingOb.disconnect();
  420. pickpocketingOb = null;
  421. pickpocketingExitOb.disconnect();
  422. pickpocketingExitOb = null;
  423. }
  424.  
  425. // Maximize extra exp (capitalization exp - total cost)
  426. class ScammingSolver {
  427. get BASE_ACTION_COST() {
  428. return 0.02;
  429. }
  430. get FAILURE_COST_MAP() {
  431. return this.algo === 'merit'
  432. ? {
  433. 1: 0,
  434. 20: 0,
  435. 40: 0,
  436. 60: 0,
  437. 80: 0,
  438. }
  439. : {
  440. 1: 1,
  441. 20: 1,
  442. 40: 1,
  443. 60: 0.5,
  444. 80: 0.33,
  445. };
  446. }
  447. get CONCERN_SUCCESS_RATE_MAP() {
  448. return {
  449. 'young adult': 0.55,
  450. 'middle-aged': 0.5,
  451. senior: 0.45,
  452. professional: 0.4,
  453. affluent: 0.35,
  454. '': 0.5,
  455. };
  456. }
  457. get CELL_VALUE_MAP() {
  458. return this.algo === 'merit'
  459. ? {
  460. low: 2,
  461. medium: 2,
  462. high: 2,
  463. fail: -20,
  464. }
  465. : {
  466. low: 0.5,
  467. medium: 1.5,
  468. high: 2.5,
  469. fail: -20, // The penalty should be -10. I add a bit to it for demoralization and chain bonus lost.
  470. };
  471. }
  472. get SAFE_CELL_SET() {
  473. return new Set(['neutral', 'low', 'medium', 'high', 'temptation']);
  474. }
  475. get DISPLACEMENT() {
  476. // prettier-ignore
  477. return {
  478. 1: {
  479. strong: [[10, 19], [15, 29], [18, 35], [21, 39], [22, 42], [23, 44]],
  480. soft: [[3, 7], [5, 11], [6, 13], [6, 14], [7, 15], [7, 16]],
  481. back: [[-4, -2], [-6, -3], [-7, -4], [-8, -4], [-9, -4], [-9, -5]],
  482. },
  483. 20: {
  484. strong: [[8, 15], [12, 23], [15, 28], [16, 31], [18, 33], [18, 35]],
  485. soft: [[3, 7], [5, 11], [6, 13], [6, 14], [7, 15], [7, 16]],
  486. back: [[-4, -2], [-6, -3], [-7, -4], [-8, -4], [-9, -4], [-9, -5]],
  487. },
  488. 40: {
  489. strong: [[7, 13], [11, 20], [13, 24], [14, 27], [15, 29], [16, 30]],
  490. soft: [[3, 6], [5, 9], [6, 11], [6, 12], [7, 13], [7, 14]],
  491. back: [[-4, -2], [-6, -3], [-7, -4], [-8, -4], [-9, -4], [-9, -5]],
  492. },
  493. 60: {
  494. strong: [[6, 11], [9, 17], [11, 20], [12, 23], [13, 24], [14, 25]],
  495. soft: [[2, 4], [3, 6], [4, 7], [4, 8], [4, 9], [5, 9]],
  496. back: [[-4, -2], [-6, -3], [-7, -4], [-8, -4], [-9, -4], [-9, -5]],
  497. },
  498. 80: {
  499. strong: [[5, 9], [8, 14], [9, 17], [10, 19], [11, 20], [12, 21]],
  500. soft: [[2, 3], [3, 5], [4, 6], [4, 6], [4, 7], [5, 7]],
  501. back: [[-3, -2], [-5, -3], [-6, -4], [-6, -4], [-7, -4], [-7, -5]],
  502. },
  503. };
  504. }
  505. get MERIT_MASK_MAP() {
  506. return {
  507. temptation: 1n << 50n,
  508. sensitivity: 1n << 51n,
  509. hesitation: 1n << 52n,
  510. concern: 1n << 53n,
  511. };
  512. }
  513. get MERIT_REQUIREMENT_MASK() {
  514. return 0xfn << 50n;
  515. }
  516.  
  517. /**
  518. * @param {'exp' | 'merit'} algo
  519. * @param {('neutral' | 'low' | 'medium' | 'high' | 'temptation' | 'sensitivity' | 'hesitation' | 'concern' | 'fail')[]} bar
  520. * @param {1 | 20 | 40 | 60 | 80} targetLevel
  521. * @param {number} round
  522. * @param {number} suspicion
  523. * @param {'young adult' | 'middle-aged' | 'senior' | 'professional' | 'affluent' | ''} mark
  524. */
  525. constructor(algo, bar, targetLevel, round, suspicion, mark) {
  526. this.algo = algo;
  527. this.bar = bar;
  528. this.targetLevel = targetLevel;
  529. this.failureCost = this.FAILURE_COST_MAP[this.targetLevel];
  530. this.initialRound = round;
  531. this.initialSuspicion = suspicion;
  532. this.mark = mark;
  533.  
  534. this.driftArrayMap = new Map(); // (resolvingBitmap) => number[50]
  535. this.dp = new Map(); // (resolvingBitmap | round) => {value: number, action: string, multi: number}[50]
  536.  
  537. this.resolvingMasks = new Array(50);
  538. for (let pip = 0; pip < 50; pip++) {
  539. if (this.resolvingMasks[pip]) {
  540. continue;
  541. }
  542. if (this.bar[pip] !== 'hesitation' && this.bar[pip] !== 'concern') {
  543. this.resolvingMasks[pip] = 0n;
  544. continue;
  545. }
  546. let mask = this.algo === 'merit' ? this.MERIT_MASK_MAP[this.bar[pip]] : 0n;
  547. for (let endPip = pip; endPip < 50 && this.bar[endPip] === this.bar[pip]; endPip++) {
  548. mask += 1n << BigInt(endPip);
  549. }
  550. for (let endPip = pip; endPip < 50 && this.bar[endPip] === this.bar[pip]; endPip++) {
  551. this.resolvingMasks[endPip] = mask;
  552. }
  553. }
  554. }
  555.  
  556. /**
  557. * @param {number} driftBitmap 1 for temptation triggered, 2 for sensitivity triggered
  558. */
  559. solve(round, pip, resolvingBitmap, multiplierUsed, driftBitmap) {
  560. if (this.algo === 'merit') {
  561. for (let pip = 0; pip < 50; pip++) {
  562. if (this._isResolved(pip, resolvingBitmap)) {
  563. resolvingBitmap |= this.MERIT_MASK_MAP[this.bar[pip]] ?? 0n;
  564. }
  565. }
  566. resolvingBitmap |= BigInt(driftBitmap) << 50n;
  567. }
  568. const result = this._visit(round - multiplierUsed, resolvingBitmap, multiplierUsed, pip);
  569. return result[pip];
  570. }
  571.  
  572. /**
  573. * @param {number} round
  574. * @param {bigint} resolvingBitmap
  575. * @param {number} minMulti
  576. * @param {number | undefined} singlePip
  577. */
  578. _visit(round, resolvingBitmap, minMulti, singlePip = undefined) {
  579. const dpKey = BigInt(round) | (resolvingBitmap << 6n);
  580. // Cached solutions do not respect `minMulti`.
  581. if (minMulti === 0) {
  582. const visited = this.dp.get(dpKey);
  583. if (visited) {
  584. return visited;
  585. }
  586. }
  587. const result = new Array(50);
  588. this.dp.set(dpKey, result);
  589. if (this._estimateSuspicion(round) >= 50) {
  590. for (let pip = 0; pip < 50; pip++) {
  591. result[pip] = this._getCellResult(pip, resolvingBitmap);
  592. }
  593. return result;
  594. }
  595. const driftArray = this._getDriftArray(resolvingBitmap);
  596. const [pipBegin, pipEnd] = singlePip !== undefined ? [singlePip, singlePip + 1] : [0, 50];
  597. for (let pip = pipBegin; pip < pipEnd; pip++) {
  598. const best = this._getCellResult(pip, resolvingBitmap);
  599. if (this.bar[pip] === 'fail') {
  600. result[pip] = best;
  601. continue;
  602. }
  603. if (!this._isResolved(pip, resolvingBitmap)) {
  604. if (this.bar[pip] === 'hesitation') {
  605. const resolvedResult = this._visit(round, resolvingBitmap | this.resolvingMasks[pip], 0);
  606. result[pip] = resolvedResult[pip];
  607. continue;
  608. }
  609. if (this.bar[pip] === 'concern') {
  610. const resolvedResult = this._visit(round + 1, resolvingBitmap | this.resolvingMasks[pip], 0);
  611. const unresolvedResult = this._visit(round + 1, resolvingBitmap, 0);
  612. const concernSuccessRate = this.CONCERN_SUCCESS_RATE_MAP[this.mark] ?? this.CONCERN_SUCCESS_RATE_MAP[''];
  613. const value =
  614. resolvedResult[pip].value * concernSuccessRate +
  615. (unresolvedResult[pip].value - this.failureCost) * (1 - concernSuccessRate) -
  616. this.BASE_ACTION_COST;
  617. result[pip] = {
  618. value: Math.max(0, value),
  619. action: value > 0 ? 'resolve' : 'abandon',
  620. multi: 0,
  621. };
  622. continue;
  623. }
  624. }
  625. for (let multi = minMulti; multi <= 5; multi++) {
  626. const suspicionAfterMulti = this._estimateSuspicion(round + multi);
  627. const nextRoundResult = this._visit(round + multi + 1, resolvingBitmap, 0);
  628. const feasibleActions = pip > 0 ? ['strong', 'soft', 'back'] : ['strong', 'soft'];
  629. for (const action of feasibleActions) {
  630. const displacementArray = this.DISPLACEMENT[this.targetLevel.toString()]?.[action]?.[multi];
  631. if (!displacementArray) {
  632. continue;
  633. }
  634. const [minDisplacement, maxDisplacement] = displacementArray;
  635. let totalValue = 0;
  636. for (let disp = minDisplacement; disp <= maxDisplacement; disp++) {
  637. const landingPip = Math.max(Math.min(pip + disp, 49), 0);
  638. const newPip = driftArray[landingPip];
  639. if (landingPip < suspicionAfterMulti || newPip < suspicionAfterMulti) {
  640. totalValue += this.CELL_VALUE_MAP.fail;
  641. } else {
  642. if (!this.SAFE_CELL_SET.has(this.bar[landingPip]) && !this._isResolved(landingPip, resolvingBitmap)) {
  643. totalValue -= this.failureCost;
  644. }
  645. totalValue -= this.BASE_ACTION_COST;
  646. const landingResult =
  647. this.algo === 'merit' && newPip !== landingPip
  648. ? this._visit(round + multi + 1, resolvingBitmap | this.MERIT_MASK_MAP[this.bar[landingPip]], 0)
  649. : nextRoundResult;
  650. totalValue += landingResult[newPip].value;
  651. }
  652. }
  653. const avgValue = totalValue / (maxDisplacement - minDisplacement + 1) - this.BASE_ACTION_COST * multi;
  654. if (avgValue > best.value) {
  655. best.value = avgValue;
  656. best.action = action;
  657. best.multi = multi;
  658. }
  659. }
  660. }
  661. result[pip] = best;
  662. }
  663. return result;
  664. }
  665.  
  666. _getDriftArray(resolvingBitmap) {
  667. const cached = this.driftArrayMap.get(resolvingBitmap);
  668. if (cached) {
  669. return cached;
  670. }
  671. const driftArray = new Array(50);
  672. this.driftArrayMap.set(resolvingBitmap, driftArray);
  673. for (let pip = 0; pip < 50; pip++) {
  674. let newPip = pip;
  675. switch (this.bar[pip]) {
  676. case 'temptation':
  677. while (
  678. newPip + 1 < 50 &&
  679. (!this.SAFE_CELL_SET.has(this.bar[newPip]) || this.bar[newPip] === 'temptation') &&
  680. !this._isResolved(newPip, resolvingBitmap)
  681. ) {
  682. newPip++;
  683. }
  684. break;
  685. case 'sensitivity':
  686. while (newPip > 0 && this.bar[newPip] !== 'neutral' && !this._isResolved(newPip, resolvingBitmap)) {
  687. newPip--;
  688. }
  689. break;
  690. }
  691. driftArray[pip] = newPip;
  692. }
  693. return driftArray;
  694. }
  695.  
  696. _getCellResult(pip, resolvingBitmap) {
  697. let value = this.CELL_VALUE_MAP[this.bar[pip]] ?? 0;
  698. if (this.algo === 'merit' && (resolvingBitmap & this.MERIT_REQUIREMENT_MASK) !== this.MERIT_REQUIREMENT_MASK) {
  699. value = Math.min(value, 0);
  700. }
  701. const action = this.bar[pip] === 'fail' ? 'fail' : value > 0 ? 'capitalize' : 'abandon';
  702. return { value, action, multi: 0 };
  703. }
  704.  
  705. _estimateSuspicion(round) {
  706. if (round <= this.initialRound) {
  707. return this.initialSuspicion;
  708. }
  709. const predefined = [0, 0, 0, 0, 2, 5, 8, 11, 16, 23, 34, 50][round] ?? 50;
  710. const current = Math.floor(this.initialSuspicion * 1.5 ** (round - this.initialRound));
  711. return Math.max(predefined, current);
  712. }
  713.  
  714. _isResolved(pip, resolvingBitmap) {
  715. return ((1n << BigInt(pip)) & resolvingBitmap) !== 0n;
  716. }
  717. }
  718.  
  719. class ScammingStore {
  720. get TARGET_LEVEL_MAP() {
  721. return {
  722. 'delivery scam': 1,
  723. 'family scam': 1,
  724. 'prize scam': 1,
  725. 'charity scam': 20,
  726. 'tech support scam': 20,
  727. 'vacation scam': 40,
  728. 'tax scam': 40,
  729. 'advance-fee scam': 60,
  730. 'job scam': 60,
  731. 'romance scam': 80,
  732. 'investment scam': 80,
  733. };
  734. }
  735. get SPAM_ID_MAP() {
  736. return {
  737. 295: 'delivery',
  738. 293: 'family',
  739. 291: 'prize',
  740. 297: 'charity',
  741. 299: 'tech support',
  742. 301: 'vacation',
  743. 303: 'tax',
  744. 305: 'advance-fee',
  745. 307: 'job',
  746. 309: 'romance',
  747. 311: 'investment',
  748. };
  749. }
  750. constructor() {
  751. this.data = getValue('scamming', {});
  752. this.data.targets = this.data.targets ?? {};
  753. this.data.farms = this.data.farms ?? {};
  754. this.data.spams = this.data.spams ?? {};
  755. this.data.defaultAlgo = this.data.defaultAlgo ?? 'exp';
  756. this.unsyncedSet = new Set(Object.keys(this.data.targets));
  757. this.solvers = {};
  758. this.lastSolutions = {};
  759. this.cash = undefined;
  760. }
  761.  
  762. update(data) {
  763. this._updateTargets(data.DB?.crimesByType?.targets);
  764. this._updateFarms(data.DB?.additionalInfo?.currentOngoing);
  765. this._updateSpams(data.DB?.currentUserStats?.crimesByIDAttempts, data.DB?.crimesByType?.methods);
  766. this.cash = data.DB?.user?.money;
  767. this._save();
  768. }
  769.  
  770. setDefaultAlgo(algo) {
  771. this.data.defaultAlgo = algo;
  772. this._save();
  773. }
  774.  
  775. changeAlgo(target) {
  776. target.algos.push(target.algos.shift());
  777. target.solution = null;
  778. this._solve(target);
  779. this._save();
  780. }
  781.  
  782. _save() {
  783. setValue('scamming', this.data);
  784. }
  785.  
  786. _updateTargets(targets) {
  787. if (!targets) {
  788. return;
  789. }
  790. for (const target of targets) {
  791. const stored = this.data.targets[target.subID];
  792. if (stored && !target.new && target.bar) {
  793. stored.driftBitmap = stored.driftBitmap ?? 0; // data migration for v1.4.6
  794. stored.turns = stored.turns ?? target.turns ?? 0; // data migration for v1.4.10
  795. stored.mark = (target.target ?? '').toLowerCase();
  796. let updated = false;
  797. if (
  798. stored.multiplierUsed !== target.multiplierUsed ||
  799. stored.pip !== target.pip ||
  800. stored.turns !== (target.turns ?? 0)
  801. ) {
  802. stored.multiplierUsed = target.multiplierUsed;
  803. stored.pip = target.pip;
  804. stored.turns = target.turns ?? 0;
  805. stored.expire = target.expire;
  806. updated = true;
  807. }
  808. if (updated && this.unsyncedSet.has(stored.id)) {
  809. stored.unsynced = true; // replied on another device
  810. }
  811. this.unsyncedSet.delete(stored.id);
  812. if (stored.bar) {
  813. for (let pip = 0; pip < 50; pip++) {
  814. if (target.bar[pip] === stored.bar[pip]) {
  815. continue;
  816. }
  817. if (target.bar[pip] === 'fail' && stored.suspicion <= pip) {
  818. stored.suspicion = pip + 1;
  819. updated = true;
  820. }
  821. if (target.bar[pip] === 'neutral' && (BigInt(stored.resolvingBitmap) & (1n << BigInt(pip))) === 0n) {
  822. stored.resolvingBitmap = (BigInt(stored.resolvingBitmap) | (1n << BigInt(pip))).toString();
  823. updated = true;
  824. }
  825. }
  826. if (target.firstPip) {
  827. if (stored.bar[target.firstPip] === 'temptation') {
  828. stored.driftBitmap |= 1;
  829. }
  830. if (stored.bar[target.firstPip] === 'sensitivity') {
  831. stored.driftBitmap |= 2;
  832. }
  833. }
  834. }
  835. if (updated) {
  836. // Round is not accurate for concern and hesitation.
  837. stored.round = stored.unsynced ? this._estimateRound(target) : stored.round + 1;
  838. }
  839. if (!stored.bar) {
  840. stored.bar = target.bar;
  841. updated = true;
  842. }
  843. if (updated || !stored.solution) {
  844. this._solve(stored);
  845. }
  846. } else {
  847. const multiplierUsed = target.multiplierUsed ?? 0;
  848. const pip = target.pip ?? 0;
  849. const round = multiplierUsed === 0 && pip === 0 ? 0 : Math.max(1, multiplierUsed);
  850. const stored = {
  851. id: target.subID,
  852. email: target.email,
  853. level: this.TARGET_LEVEL_MAP[target.scamMethod.toLowerCase()] ?? 999,
  854. mark: '',
  855. round,
  856. turns: target.turns ?? 0,
  857. multiplierUsed,
  858. pip,
  859. expire: target.expire,
  860. bar: target.bar ?? null,
  861. suspicion: 0,
  862. resolvingBitmap: '0',
  863. driftBitmap: 0,
  864. algos: null,
  865. solution: null,
  866. unsynced: round > 0,
  867. };
  868. this.data.targets[target.subID] = stored;
  869. this._solve(stored);
  870. }
  871. }
  872. const now = Math.floor(Date.now() / 1000);
  873. for (const target of Object.values(this.data.targets)) {
  874. if (target.expire < now) {
  875. delete this.data.targets[target.id];
  876. }
  877. }
  878. }
  879.  
  880. _updateFarms(currentOngoing) {
  881. if (typeof currentOngoing !== 'object' || !(currentOngoing.length > 0)) {
  882. return;
  883. }
  884. for (const item of currentOngoing) {
  885. if (!item.type) {
  886. continue;
  887. }
  888. this.data.farms[item.type] = { expire: item.timeEnded };
  889. }
  890. }
  891.  
  892. _updateSpams(crimesByIDAttempts, methods) {
  893. if (!crimesByIDAttempts || !methods) {
  894. return;
  895. }
  896. const now = Math.floor(Date.now() / 1000);
  897. for (const [id, count] of Object.entries(crimesByIDAttempts)) {
  898. const type = this.SPAM_ID_MAP[id];
  899. const method = methods.find((x) => String(x.crimeID) === id);
  900. if (!type || !method) {
  901. continue;
  902. }
  903. const stored = this.data.spams[id];
  904. if (stored) {
  905. if (count !== stored.count) {
  906. stored.count = count;
  907. stored.accurate = now - stored.ts < 3600;
  908. stored.since = now;
  909. }
  910. stored.ts = now;
  911. stored.depreciation = method.depreciation;
  912. } else {
  913. this.data.spams[id] = {
  914. count,
  915. accurate: false,
  916. since: null,
  917. ts: now,
  918. depreciation: method.depreciation,
  919. };
  920. }
  921. }
  922. }
  923.  
  924. _solve(target) {
  925. if (!target.bar) {
  926. return;
  927. }
  928. this.lastSolutions[target.id] = target.solution;
  929. let solver = this.solvers[target.id];
  930. if (!solver || solver.algo !== target.algos?.[0] || target.suspicion > 0) {
  931. if (!target.algos) {
  932. target.algos = this._isMeritFeasible(target) ? ['exp', 'merit'] : ['exp'];
  933. const defaultIndex = target.algos.indexOf(this.data.defaultAlgo);
  934. if (defaultIndex > 0) {
  935. target.algos = [...target.algos.slice(defaultIndex), ...target.algos.slice(0, defaultIndex)];
  936. }
  937. }
  938. solver = new ScammingSolver(target.algos[0], target.bar, target.level, target.round, target.suspicion);
  939. this.solvers[target.id] = solver;
  940. }
  941. target.solution = solver.solve(
  942. target.round,
  943. target.pip,
  944. BigInt(target.resolvingBitmap),
  945. target.multiplierUsed,
  946. target.driftBitmap,
  947. );
  948. }
  949.  
  950. _estimateRound(target) {
  951. // This "turns" from the server gets +2 from temptation and sensitivity (round +1 in these cases) and
  952. // gets +1 from hesitation (round +2 in this case).
  953. // The "*Attempt" fields from the server are at most 1 even with multiple attempts.
  954. return Math.max(
  955. 0,
  956. (target.turns ?? 0) -
  957. (target.temptationAttempt ?? 0) -
  958. (target.sensitivityAttempt ?? 0) +
  959. (target.hesitationAttempt ?? 0),
  960. );
  961. }
  962.  
  963. _isMeritFeasible(target) {
  964. const cells = new Set(target.bar);
  965. return cells.has('temptation') && cells.has('sensitivity') && cells.has('hesitation') && cells.has('concern');
  966. }
  967. }
  968.  
  969. class ScammingObserver {
  970. constructor() {
  971. this.store = new ScammingStore();
  972. this.crimeOptions = null;
  973. this.farmIcons = null;
  974. this.spamOptions = null;
  975. this.virtualLists = null;
  976. this.observer = new MutationObserver((mutations) => {
  977. const isAdd = mutations.some((mutation) => {
  978. for (const added of mutation.addedNodes) {
  979. if (added instanceof HTMLElement) {
  980. return true;
  981. }
  982. }
  983. return false;
  984. });
  985. if (!isAdd) {
  986. return;
  987. }
  988. for (const element of this.crimeOptions) {
  989. if (!element.classList.contains('cm-sc-seen')) {
  990. element.classList.add('cm-sc-seen');
  991. this._refreshCrimeOption(element);
  992. }
  993. }
  994. for (const element of this.farmIcons) {
  995. if (!element.classList.contains('cm-sc-seen')) {
  996. element.classList.add('cm-sc-seen');
  997. this._refreshFarm(element);
  998. }
  999. }
  1000. for (const element of this.spamOptions) {
  1001. if (!element.classList.contains('cm-sc-seen')) {
  1002. element.classList.add('cm-sc-seen');
  1003. this._refreshSpam(element);
  1004. }
  1005. }
  1006. for (const element of this.virtualLists) {
  1007. if (!element.classList.contains('cm-sc-seen')) {
  1008. element.classList.add('cm-sc-seen');
  1009. this._refreshSettings(element);
  1010. }
  1011. }
  1012. });
  1013. }
  1014.  
  1015. start() {
  1016. if (this.crimeOptions) {
  1017. return;
  1018. }
  1019. this.crimeOptions = document.body.getElementsByClassName('crime-option');
  1020. this.farmIcons = document.body.getElementsByClassName('scraperPhisher___oy1Wn');
  1021. this.spamOptions = document.body.getElementsByClassName('optionWithLevelRequirement___cHH35');
  1022. this.virtualLists = document.body.getElementsByClassName('virtualList___noLef');
  1023. this.observer.observe($('.scamming-root')[0], { subtree: true, childList: true });
  1024. }
  1025.  
  1026. stop() {
  1027. this.crimeOptions = null;
  1028. this.observer.disconnect();
  1029. }
  1030.  
  1031. onNewData() {
  1032. this.start();
  1033. for (const element of this.crimeOptions) {
  1034. this._refreshCrimeOption(element);
  1035. }
  1036. for (const element of this.farmIcons) {
  1037. this._refreshFarm(element);
  1038. }
  1039. for (const element of this.spamOptions) {
  1040. this._refreshSpam(element);
  1041. }
  1042. }
  1043.  
  1044. _buildHintHtml(target, solution, lastSolution) {
  1045. const actionText =
  1046. {
  1047. strong: 'Fast Fwd',
  1048. soft: 'Soft Fwd',
  1049. back: 'Back',
  1050. capitalize: '$$$',
  1051. abandon: 'Abandon',
  1052. resolve: 'Resolve',
  1053. }[solution.action] ?? 'N/A';
  1054. const algoText =
  1055. {
  1056. exp: 'Exp',
  1057. merit: 'Merit',
  1058. }[target.algos?.[0]] ?? 'Score';
  1059. const score = Math.floor(solution.value * 100);
  1060. const scoreColor = score < 30 ? 't-red' : score < 100 ? 't-yellow' : 't-green';
  1061. const scoreDiff = lastSolution ? score - Math.floor(lastSolution.value * 100) : 0;
  1062. const scoreDiffColor = scoreDiff > 0 ? 't-green' : 't-red';
  1063. const scoreDiffText = scoreDiff !== 0 ? `(${scoreDiff > 0 ? '+' : ''}${scoreDiff})` : '';
  1064. let rspText = solution.multi > target.multiplierUsed ? 'Accel' : actionText;
  1065. let rspColor = '';
  1066. let fullRspText = solution.multi > 0 ? `(${target.multiplierUsed}/${solution.multi} + ${actionText})` : '';
  1067. if (target.unsynced) {
  1068. rspText = 'Unsynced';
  1069. rspColor = 't-gray-c';
  1070. fullRspText = fullRspText !== '' ? fullRspText : `(${actionText})`;
  1071. }
  1072. return `<span class="cm-sc-info cm-sc-hint cm-sc-hint-content">
  1073. <span><span class="cm-sc-algo">${algoText}</span>: <span class="${scoreColor}">${score}</span><span class="${scoreDiffColor}">${scoreDiffText}</span></span>
  1074. <span class="cm-sc-hint-action"><span class="${rspColor}">${rspText}</span> <span class="t-gray-c">${fullRspText}</span></span>
  1075. <span class="cm-sc-hint-button t-blue">Lv${target.level}</span>
  1076. </span>`;
  1077. }
  1078.  
  1079. _refreshCrimeOption(element) {
  1080. this._refreshTarget(element);
  1081. this._refreshFarmButton(element);
  1082. }
  1083.  
  1084. _refreshTarget(element) {
  1085. const $crimeOption = $(element);
  1086. const $email = $crimeOption.find('span.email___gVRXx');
  1087. const email = $email.text();
  1088. const target = Object.values(this.store.data.targets).find((x) => x.email === email);
  1089. if (!target) {
  1090. return;
  1091. }
  1092. // clear old info elements
  1093. const hasHint = $crimeOption.find('.cm-sc-hint-content').length > 0;
  1094. $crimeOption.find('.cm-sc-info').remove();
  1095. $email.parent().addClass('cm-sc-info-wrapper');
  1096. $email.parent().children().addClass('cm-sc-orig-info');
  1097. // hint
  1098. const solution = target.solution;
  1099. if (solution) {
  1100. if (!hasHint) {
  1101. $email.parent().removeClass('cm-sc-hint-hidden');
  1102. }
  1103. $crimeOption.attr('data-cm-action', solution.multi > target.multiplierUsed ? 'accelerate' : solution.action);
  1104. $crimeOption.toggleClass('cm-sc-unsynced', target.unsynced ?? false);
  1105. const lastSolution = this.store.lastSolutions[target.id];
  1106. $email.parent().append(this._buildHintHtml(target, solution, lastSolution));
  1107. $email.parent().append(`<span class="cm-sc-info cm-sc-orig-info cm-sc-hint-button t-blue">Hint</div>`);
  1108. $crimeOption.find('.cm-sc-hint-button').on('click', () => {
  1109. $email.parent().toggleClass('cm-sc-hint-hidden');
  1110. });
  1111. if (target.algos?.length > 1) {
  1112. const $algo = $crimeOption.find('.cm-sc-algo');
  1113. $algo.addClass('t-blue');
  1114. $algo.addClass('cm-sc-active');
  1115. $algo.on('click', () => {
  1116. this.store.changeAlgo(target);
  1117. this._refreshTarget(element);
  1118. });
  1119. }
  1120. } else {
  1121. $email.parent().addClass('cm-sc-hint-hidden');
  1122. }
  1123. // lifetime
  1124. const now = Math.floor(Date.now() / 1000);
  1125. const lifetime = formatLifetime(target.expire - now);
  1126. $email.before(`<span class="cm-sc-info ${lifetime.color}">${lifetime.text}</div>`);
  1127. // scale
  1128. const $cells = $crimeOption.find('.cell___AfwZm');
  1129. if ($cells.length >= 50) {
  1130. $cells.find('.cm-sc-scale').remove();
  1131. // Ignore cells after the first 50, which are faded out soon
  1132. for (let i = 0; i < 50; i++) {
  1133. const dist = i - target.pip;
  1134. const label = dist % 5 !== 0 || dist === 0 || dist < -5 ? '' : dist % 10 === 0 ? (dist / 10).toString() : "'";
  1135. let $scale = $cells.eq(i).children('.cm-sc-scale');
  1136. if ($scale.length === 0) {
  1137. $scale = $('<div class="cm-sc-scale"></div>');
  1138. $cells.eq(i).append($scale);
  1139. }
  1140. $scale.text(label);
  1141. }
  1142. }
  1143. // multiplier
  1144. const $accButton = $crimeOption.find('.response-type-button').eq(3);
  1145. $accButton.find('.cm-sc-multiplier').remove();
  1146. if (target.multiplierUsed > 0) {
  1147. $accButton.append(`<div class="cm-sc-multiplier">${target.multiplierUsed}</div>`);
  1148. }
  1149. }
  1150.  
  1151. _refreshFarmButton(element) {
  1152. const $element = $(element);
  1153. if ($element.find('.emailAddresses___ky_qG').length === 0) {
  1154. return;
  1155. }
  1156. $element.find('.commitButtonSection___wJfnI button').toggleClass('cm-sc-low-cash', this.store.cash < 10000);
  1157. }
  1158.  
  1159. _refreshFarm(element) {
  1160. const $element = $(element);
  1161. const label = $element.attr('aria-label') ?? '';
  1162. const farm = Object.entries(this.store.data.farms).find(([type]) => label.toLowerCase().includes(type))?.[1];
  1163. if (!farm) {
  1164. return;
  1165. }
  1166. const now = Math.floor(Date.now() / 1000);
  1167. const lifetime = formatLifetime(farm.expire - now);
  1168. $element.find('.cm-sc-farm-lifetime').remove();
  1169. $element.append(`<div class="cm-sc-farm-lifetime ${lifetime.color}">${lifetime.text}</div>`);
  1170. }
  1171.  
  1172. _refreshSpam(element) {
  1173. const $spamOption = $(element);
  1174. if ($spamOption.closest('.dropdownList').length === 0) {
  1175. return;
  1176. }
  1177. const label = $spamOption
  1178. .contents()
  1179. .filter((_, x) => x.nodeType === Node.TEXT_NODE)
  1180. .text();
  1181. const spam = Object.entries(this.store.data.spams).find(([id]) =>
  1182. label.toLowerCase().includes(this.store.SPAM_ID_MAP[id]),
  1183. )?.[1];
  1184. $spamOption.addClass('cm-sc-spam-option');
  1185. $spamOption.find('.cm-sc-spam-elapsed').remove();
  1186. if (!spam || !spam.since || spam.depreciation) {
  1187. return;
  1188. }
  1189. const now = Math.floor(Date.now() / 1000);
  1190. const elapsed = formatLifetime(now - spam.since);
  1191. if (!spam.accurate) {
  1192. elapsed.text = '> ' + elapsed.text;
  1193. }
  1194. if (elapsed.hours >= 24 * 8) {
  1195. elapsed.text = '> 7d';
  1196. }
  1197. $spamOption.append(`<div class="cm-sc-spam-elapsed ${elapsed.color}">${elapsed.text}</div>`);
  1198. }
  1199.  
  1200. _refreshSettings(element) {
  1201. const store = this.store;
  1202. const defaultAlgo = store.data.defaultAlgo;
  1203. const $settings = $(`<div class="cm-sc-settings">
  1204. <span>Default Strategy:</span>
  1205. <span class="cm-sc-algo-option t-blue" data-cm-value="exp">Exp</span>
  1206. <span class="cm-sc-algo-option t-blue" data-cm-value="merit">Merit</span>
  1207. </div>`);
  1208. $settings.children(`[data-cm-value="${defaultAlgo}"]`).addClass('cm-sc-active');
  1209. $settings.children('.cm-sc-algo-option').on('click', function () {
  1210. const $this = $(this);
  1211. store.setDefaultAlgo($this.attr('data-cm-value'));
  1212. $this.siblings().removeClass('cm-sc-active');
  1213. $this.addClass('cm-sc-active');
  1214. });
  1215. $settings.insertBefore(element);
  1216. }
  1217. }
  1218. const scammingObserver = new ScammingObserver();
  1219.  
  1220. async function checkScamming(crimeType, data) {
  1221. if (crimeType !== '12') {
  1222. scammingObserver.stop();
  1223. return;
  1224. }
  1225. scammingObserver.store.update(data);
  1226. scammingObserver.onNewData();
  1227. }
  1228.  
  1229. async function onCrimeData(crimeType, data) {
  1230. await checkDemoralization(data);
  1231. await checkBurglary(crimeType, data);
  1232. await checkPickpocketing(crimeType);
  1233. await checkScamming(crimeType, data);
  1234. }
  1235.  
  1236. function interceptFetch() {
  1237. const targetWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
  1238. const origFetch = targetWindow.fetch;
  1239. targetWindow.fetch = async (...args) => {
  1240. const rsp = await origFetch(...args);
  1241.  
  1242. try {
  1243. const url = new URL(args[0], location.origin);
  1244. const params = new URLSearchParams(url.search);
  1245. const reqBody = args[1]?.body;
  1246. const crimeType = params.get('typeID') ?? reqBody?.get('typeID');
  1247. if (url.pathname === '/page.php' && params.get('sid') === 'crimesData' && crimeType) {
  1248. const clonedRsp = rsp.clone();
  1249. await onCrimeData(crimeType, await clonedRsp.json());
  1250. }
  1251. } catch {
  1252. // ignore
  1253. }
  1254.  
  1255. return rsp;
  1256. };
  1257. }
  1258.  
  1259. function renderMorale() {
  1260. const interval = setInterval(async function () {
  1261. if (!$) {
  1262. return; // JQuery is not loaded in TornPDA yet
  1263. }
  1264. const $container = $('.crimes-app-header');
  1265. if ($container.length === 0) {
  1266. return;
  1267. }
  1268. clearInterval(interval);
  1269. $container.append(`<span>Morale: <span id="crime-morale-value">-</span>%</span>`);
  1270. const morale = parseInt(await getValue(STORAGE_MORALE));
  1271. if (!isNaN(morale)) {
  1272. updateMorale(morale);
  1273. }
  1274. // Show hidden debug button on double-click
  1275. let lastClick = 0; // dblclick event doesn't work well on mobile
  1276. $('#crime-morale-value')
  1277. .parent()
  1278. .on('click', function () {
  1279. if (Date.now() - lastClick > 1000) {
  1280. lastClick = Date.now();
  1281. return;
  1282. }
  1283. const data = {
  1284. morale: getValue(STORAGE_MORALE),
  1285. burglary: getValue('burglary'),
  1286. scamming: getValue('scamming'),
  1287. };
  1288. const export_uri = `data:application/json;charset=utf-8,${encodeURIComponent(JSON.stringify(data))}`;
  1289. $(this).replaceWith(`<a download="crime-morale-debug.json" href="${export_uri}"
  1290. class="torn-btn" style="display:inline-block;">Export Debug Data</a>`);
  1291. });
  1292. }, 500);
  1293. }
  1294.  
  1295. function updateMorale(morale) {
  1296. $('#crime-morale-value').text(morale.toString());
  1297. }
  1298.  
  1299. function renderStyle() {
  1300. addStyle(`
  1301. .cm-bg-lifetime {
  1302. position: absolute;
  1303. top: 0;
  1304. right: 0;
  1305. padding: 2px;
  1306. background: var(--default-bg-panel-color);
  1307. border: 1px solid darkgray;
  1308. }
  1309. .cm-bg-favor {
  1310. position: absolute;
  1311. right: 0;
  1312. bottom: 0;
  1313. background: #fffc;
  1314. height: 20px;
  1315. width: 20px;
  1316. font-size: 20px;
  1317. line-height: 1;
  1318. cursor: pointer;
  1319. pointer-events: auto !important;
  1320. }
  1321. .cm-bg-favor:after {
  1322. content: '\u2606';
  1323. display: block;
  1324. width: 100%;
  1325. height: 100%;
  1326. text-align: center;
  1327. }
  1328. .cm-bg-favor.cm-bg-active:after {
  1329. content: '\u2605';
  1330. color: orange;
  1331. }
  1332.  
  1333. :root {
  1334. --cm-pp-level-1: #37b24d;
  1335. --cm-pp-level-2: #95af14;
  1336. --cm-pp-level-3: #f4cc00;
  1337. --cm-pp-level-4: #fa9201;
  1338. --cm-pp-level-5: #e01111;
  1339. --cm-pp-level-6: #a016eb;
  1340. --cm-pp-filter-level-1: brightness(0) saturate(100%) invert(61%) sepia(11%) saturate(2432%) hue-rotate(79deg) brightness(91%) contrast(96%);
  1341. --cm-pp-filter-level-2: brightness(0) saturate(100%) invert(62%) sepia(80%) saturate(2102%) hue-rotate(32deg) brightness(99%) contrast(84%);
  1342. --cm-pp-filter-level-3: brightness(0) saturate(100%) invert(71%) sepia(53%) saturate(1820%) hue-rotate(9deg) brightness(107%) contrast(102%);
  1343. --cm-pp-filter-level-4: brightness(0) saturate(100%) invert(61%) sepia(62%) saturate(1582%) hue-rotate(356deg) brightness(94%) contrast(108%);
  1344. --cm-pp-filter-level-5: brightness(0) saturate(100%) invert(12%) sepia(72%) saturate(5597%) hue-rotate(354deg) brightness(105%) contrast(101%);
  1345. --cm-pp-filter-level-6: brightness(0) saturate(100%) invert(26%) sepia(84%) saturate(4389%) hue-rotate(271deg) brightness(86%) contrast(119%);
  1346. }
  1347. @keyframes cm-fade-out {
  1348. from {
  1349. opacity: 1;
  1350. }
  1351. to {
  1352. opacity: 0;
  1353. visibility: hidden;
  1354. }
  1355. }
  1356. .cm-overlay {
  1357. position: relative;
  1358. }
  1359. .cm-overlay:after {
  1360. content: '';
  1361. position: absolute;
  1362. background: repeating-linear-gradient(135deg, #2223, #2223 70px, #0003 70px, #0003 80px);
  1363. top: 0;
  1364. left: 0;
  1365. width: 100%;
  1366. height: 100%;
  1367. z-index: 900000;
  1368. }
  1369. .cm-overlay-fade:after {
  1370. animation-name: cm-fade-out;
  1371. animation-duration: 0.2s;
  1372. animation-timing-function: ease-in;
  1373. animation-fill-mode: forwards;
  1374. animation-delay: 0.4s
  1375. }
  1376. .cm-pp-level-1 {
  1377. color: var(--cm-pp-level-1);
  1378. }
  1379. .cm-pp-level-2 {
  1380. color: var(--cm-pp-level-2);
  1381. }
  1382. .cm-pp-level-3 {
  1383. color: var(--cm-pp-level-3);
  1384. }
  1385. .cm-pp-level-4 {
  1386. color: var(--cm-pp-level-4);
  1387. }
  1388. .cm-pp-level-5 {
  1389. color: var(--cm-pp-level-5);
  1390. }
  1391. .cm-pp-level-6 {
  1392. color: var(--cm-pp-level-6);
  1393. }
  1394. .cm-pp-level-1 [class*=timerCircle___] [class*=icon___] {
  1395. filter: var(--cm-pp-filter-level-1);
  1396. }
  1397. .cm-pp-level-2 [class*=timerCircle___] [class*=icon___] {
  1398. filter: var(--cm-pp-filter-level-2);
  1399. }
  1400. .cm-pp-level-3 [class*=timerCircle___] [class*=icon___] {
  1401. filter: var(--cm-pp-filter-level-3);
  1402. }
  1403. .cm-pp-level-4 [class*=timerCircle___] [class*=icon___] {
  1404. filter: var(--cm-pp-filter-level-4);
  1405. }
  1406. .cm-pp-level-5 [class*=timerCircle___] [class*=icon___] {
  1407. filter: var(--cm-pp-filter-level-5);
  1408. }
  1409. .cm-pp-level-6 [class*=timerCircle___] [class*=icon___] {
  1410. filter: var(--cm-pp-filter-level-6);
  1411. }
  1412. .cm-pp-level-1 [class*=timerCircle___] .CircularProgressbar-path {
  1413. stroke: var(--cm-pp-level-1) !important;
  1414. }
  1415. .cm-pp-level-2 [class*=timerCircle___] .CircularProgressbar-path {
  1416. stroke: var(--cm-pp-level-2) !important;
  1417. }
  1418. .cm-pp-level-3 [class*=timerCircle___] .CircularProgressbar-path {
  1419. stroke: var(--cm-pp-level-3) !important;
  1420. }
  1421. .cm-pp-level-4 [class*=timerCircle___] .CircularProgressbar-path {
  1422. stroke: var(--cm-pp-level-4) !important;
  1423. }
  1424. .cm-pp-level-5 [class*=timerCircle___] .CircularProgressbar-path {
  1425. stroke: var(--cm-pp-level-5) !important;
  1426. }
  1427. .cm-pp-level-6 [class*=timerCircle___] .CircularProgressbar-path {
  1428. stroke: var(--cm-pp-level-6) !important;
  1429. }
  1430. .cm-pp-level-1 [class*=commitButton___] {
  1431. border: 2px solid var(--cm-pp-level-1);
  1432. }
  1433. .cm-pp-level-2 [class*=commitButton___] {
  1434. border: 2px solid var(--cm-pp-level-2);
  1435. }
  1436. .cm-pp-level-3 [class*=commitButton___] {
  1437. border: 2px solid var(--cm-pp-level-3);
  1438. }
  1439. .cm-pp-level-4 [class*=commitButton___] {
  1440. border: 2px solid var(--cm-pp-level-4);
  1441. }
  1442. .cm-pp-level-5 [class*=commitButton___] {
  1443. border: 2px solid var(--cm-pp-level-5);
  1444. }
  1445. .cm-pp-best-build:not(.crime-option-locked) [class*=physicalPropsButton___]:before {
  1446. content: '\u2713 ';
  1447. font-weight: bold;
  1448. color: var(--cm-pp-level-2);
  1449. }
  1450.  
  1451. .cm-sc-info {
  1452. transform: translateY(1px);
  1453. }
  1454. .cm-sc-hint-button {
  1455. cursor: pointer;
  1456. }
  1457. .cm-sc-info-wrapper.cm-sc-hint-hidden > .cm-sc-hint,
  1458. .cm-sc-info-wrapper:not(.cm-sc-hint-hidden) > .cm-sc-orig-info {
  1459. display: none;
  1460. }
  1461. .cm-sc-hint-content {
  1462. display: flex;
  1463. justify-content: space-between;
  1464. flex-grow: 1;
  1465. gap: 5px;
  1466. white-space: nowrap;
  1467. overflow: hidden;
  1468. }
  1469. .cm-sc-hint-action {
  1470. flex-shrink: 1;
  1471. overflow: hidden;
  1472. text-overflow: ellipsis;
  1473. }
  1474. .cm-sc-seen[data-cm-action=strong] .response-type-button:nth-child(1):after,
  1475. .cm-sc-seen[data-cm-action=soft] .response-type-button:nth-child(2):after,
  1476. .cm-sc-seen[data-cm-action=back] .response-type-button:nth-child(3):after,
  1477. .cm-sc-seen[data-cm-action=accelerate] .response-type-button:nth-child(4):after,
  1478. .cm-sc-seen[data-cm-action=capitalize] .response-type-button:nth-child(5):after {
  1479. content: '\u2713';
  1480. color: var(--crimes-green-color);
  1481. position: absolute;
  1482. top: 0;
  1483. right: 0;
  1484. font-size: 12px;
  1485. font-weight: bolder;
  1486. line-height: 1;
  1487. z-index: 999;
  1488. }
  1489. .cm-sc-seen.cm-sc-unsynced[data-cm-action=strong] .response-type-button:nth-child(1):after,
  1490. .cm-sc-seen.cm-sc-unsynced[data-cm-action=soft] .response-type-button:nth-child(2):after,
  1491. .cm-sc-seen.cm-sc-unsynced[data-cm-action=back] .response-type-button:nth-child(3):after,
  1492. .cm-sc-seen.cm-sc-unsynced[data-cm-action=accelerate] .response-type-button:nth-child(4):after,
  1493. .cm-sc-seen.cm-sc-unsynced[data-cm-action=capitalize] .response-type-button:nth-child(5):after {
  1494. content: '?';
  1495. }
  1496. .cm-sc-seen[data-cm-action=abandon] .response-type-button:after {
  1497. content: '\u2715';
  1498. color: var(--crimes-stats-criticalFails-color);
  1499. position: absolute;
  1500. top: 0;
  1501. right: 0;
  1502. font-size: 12px;
  1503. font-weight: bolder;
  1504. line-height: 1;
  1505. z-index: 999;
  1506. }
  1507. .cm-sc-scale {
  1508. position: absolute;
  1509. top: 0;
  1510. left: 0;
  1511. width: 100%;
  1512. height: calc(100% + 10px);
  1513. line-height: 1;
  1514. font-size: 8px;
  1515. display: flex;
  1516. align-items: flex-end;
  1517. justify-content: center;
  1518. }
  1519. .cm-sc-multiplier {
  1520. position: absolute;
  1521. bottom: 0;
  1522. right: 0;
  1523. text-align: right;
  1524. font-size: 10px;
  1525. line-height: 1;
  1526. }
  1527. .cm-sc-farm-lifetime {
  1528. padding-top: 2px;
  1529. text-align: center;
  1530. }
  1531. .cm-sc-spam-option .levelLabel___LNbg8,
  1532. .cm-sc-spam-option .separator___C2skk {
  1533. display: none;
  1534. }
  1535. .cm-sc-spam-elapsed {
  1536. position: absolute;
  1537. right: -5px;
  1538. }
  1539. .cm-sc-settings {
  1540. height: 40px;
  1541. width: 100%;
  1542. background: var(--default-bg-panel-color););
  1543. border-bottom: 1px solid var(--crimes-crimeOption-borderBottomColor);
  1544. padding-left: 10px;
  1545. box-sizing: border-box;
  1546. display: flex;
  1547. align-items: center;
  1548. gap: 20px;
  1549. }
  1550. .cm-sc-algo-option {
  1551. cursor: pointer;
  1552. line-height: 1.5;
  1553. border-top: 2px solid #0000;
  1554. border-bottom: 2px solid #0000;
  1555. }
  1556. .cm-sc-algo-option.cm-sc-active {
  1557. border-bottom-color: var(--default-blue-color);
  1558. }
  1559. .cm-sc-algo.cm-sc-active {
  1560. cursor: pointer;
  1561. }
  1562. .cm-sc-algo.cm-sc-active:before {
  1563. content: '\u21bb ';
  1564. }
  1565. .cm-sc-low-cash:after {
  1566. content: 'Low Cash';
  1567. color: var(--default-red-color);
  1568. position: absolute;
  1569. width: 100%;
  1570. left: 0;
  1571. top: calc(100% - 4px);
  1572. line-height: 1;
  1573. font-size: 12px;
  1574. }
  1575. `);
  1576. }
  1577.  
  1578. interceptFetch();
  1579. renderMorale();
  1580.  
  1581. if (document.readyState === 'loading') {
  1582. document.addEventListener('readystatechange', () => {
  1583. if (document.readyState === 'interactive') {
  1584. renderStyle();
  1585. }
  1586. });
  1587. } else {
  1588. renderStyle();
  1589. }
  1590. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址