// ==UserScript==
// @name Crime Morale
// @namespace https://github.com/tobytorn
// @description A comprehensive tool for Crime 2.0
// @author tobytorn [1617955]
// @match https://www.torn.com/loader.php?sid=crimes*
// @version 1.4.9
// @grant GM_getValue
// @grant GM_setValue
// @grant unsafeWindow
// @run-at document-start
// @supportURL https://github.com/tobytorn/crime-morale
// @license MIT
// @require https://unpkg.com/[email protected]/dist/jquery.min.js
// ==/UserScript==
(function () {
'use strict';
// Avoid duplicate injection in TornPDA
if (window.CRIME_MORALE_INJECTED) {
return;
}
window.CRIME_MORALE_INJECTED = true;
console.log('Userscript Crime Morale starts');
const LOCAL_STORAGE_PREFIX = 'CRIME_MORALE_';
const STORAGE_MORALE = 'morale';
const STYLE_ELEMENT_ID = 'CRIME-MORALE-STYLE';
function getLocalStorage(key, defaultValue) {
const value = window.localStorage.getItem(LOCAL_STORAGE_PREFIX + key);
try {
return JSON.parse(value) ?? defaultValue;
} catch (err) {
return defaultValue;
}
}
function setLocalStorage(key, value) {
window.localStorage.setItem(LOCAL_STORAGE_PREFIX + key, JSON.stringify(value));
}
const isPda = window.GM_info?.scriptHandler?.toLowerCase().includes('tornpda');
const [getValue, setValue] =
isPda || typeof window.GM_getValue !== 'function' || typeof window.GM_setValue !== 'function'
? [getLocalStorage, setLocalStorage]
: [window.GM_getValue, window.GM_setValue];
function addStyle(css) {
const style =
document.getElementById(STYLE_ELEMENT_ID) ??
(function () {
const style = document.createElement('style');
style.id = STYLE_ELEMENT_ID;
document.head.appendChild(style);
return style;
})();
style.appendChild(document.createTextNode(css));
}
function formatLifetime(seconds) {
const hours = Math.floor(seconds / 3600);
const text =
hours >= 72
? `${Math.floor(hours / 24)}d`
: hours > 0
? `${hours}h`
: seconds >= 0
? `${Math.floor(seconds / 60)}m`
: '';
const color = hours >= 24 ? 't-gray-c' : hours >= 12 ? 't-yellow' : hours >= 0 ? 't-red' : '';
return { seconds, hours, text, color };
}
async function checkDemoralization(data) {
const demMod = (data.DB || {}).demMod;
if (typeof demMod !== 'number') {
return;
}
const morale = 100 - demMod;
updateMorale(morale);
await setValue(STORAGE_MORALE, morale);
}
class BurglaryObserver {
constructor() {
this.data = getValue('burglary', {});
this.data.favorite = this.data.favorite ?? [];
this.properties = null;
this.crimeOptions = null;
this.observer = new MutationObserver((mutations) => {
const isAdd = mutations.some((mutation) => {
for (const added of mutation.addedNodes) {
if (added instanceof HTMLElement) {
return true;
}
}
return false;
});
if (!isAdd) {
return;
}
for (const element of this.crimeOptions) {
if (!element.classList.contains('cm-bg-seen')) {
element.classList.add('cm-bg-seen');
this._refreshCrimeOption(element);
}
}
});
}
start() {
if (this.crimeOptions) {
return;
}
this.crimeOptions = document.body.getElementsByClassName('crime-option');
this.observer.observe($('.burglary-root')[0], { subtree: true, childList: true });
}
stop() {
this.crimeOptions = null;
this.observer.disconnect();
}
onNewData(data) {
this.start();
this.properties = data.DB?.crimesByType?.properties;
this._refreshCrimeOptions();
}
_refreshCrimeOptions() {
for (const element of this.crimeOptions) {
this._refreshCrimeOption(element);
}
}
_refreshCrimeOption(element) {
if (!this.properties) {
return;
}
const $element = $(element);
const $title = $element.find('[class*=crimeOptionSection___]').first();
$title.find('.cm-bg-lifetime').remove();
const guessedProperty = this._guessCrimeOptionData($element);
const property = this._checkCrimeOptionData($element, guessedProperty);
if (!property) {
$element.removeAttr('data-cm-id');
return;
}
$element.attr('data-cm-id', property.subID);
const now = Math.floor(Date.now() / 1000);
const lifetime = formatLifetime(property.expire - now);
if (lifetime.hours >= 0) {
$title.css('position', 'relative');
$title.append(`<div class="cm-bg-lifetime ${lifetime.color}">${lifetime.text}</div>`);
}
$element.find('.cm-bg-favor').remove();
const $favor = $('<div class="cm-bg-favor"></div>');
$favor.toggleClass('cm-bg-active', this.data.favorite.includes(property.title));
$element.find('.crime-image').append($favor);
$favor.on('click', () => {
this._toggleFavorite(property.title);
this._refreshCrimeOptions();
});
}
_guessCrimeOptionData($crimeOption) {
const savedId = $crimeOption.attr('data-cm-id');
if (savedId) {
return this.properties.find((x) => x.subID === savedId);
}
const $item = $crimeOption.closest('.virtual-item');
if ($item.prev().hasClass('lastOfGroup___YNUeQ')) {
return this.properties[0];
}
let prevId = undefined;
$item.prevAll().each(function () {
prevId = $(this).find('.crime-option[data-cm-id]').attr('data-cm-id');
if (prevId) {
return false; // break the loop
}
});
const prevIndex = this.properties.findIndex((x) => prevId && x.subID === prevId);
if (prevIndex >= 0) {
// Since we always scan crime options in document order,
// $prevItemWithId and $item should correspond to adjacent data entries.
return this.properties[prevIndex + 1];
}
if ($item.index() === 0) {
const $nextOptionWithId = $item.nextAll().find('.crime-option[data-cm-id]').first();
const nextId = $nextOptionWithId.attr('data-cm-id');
const nextIndex = this.properties.findIndex((x) => x.subID && x.subID === nextId);
const nextPos = $nextOptionWithId.closest('.virtual-item').index();
if (nextIndex >= 0 && nextPos >= 0) {
return this.properties[nextIndex - nextPos];
}
}
return undefined;
}
_checkCrimeOptionData($crimeOption, property) {
if (property === undefined) {
return undefined;
}
const { title, titleType } = this._getCrimeOptionTitle($crimeOption);
return titleType && property[titleType] === title ? property : undefined;
}
_getCrimeOptionTitle($crimeOption) {
const mobileTitle = $crimeOption.find('.title___kOWyb').text();
if (mobileTitle !== '') {
return { title: mobileTitle, titleType: 'mobileTitle' };
}
const textNode = $crimeOption.find('.crimeOptionSection___hslpu')[0]?.firstChild;
if (textNode?.nodeType === Node.TEXT_NODE) {
return { title: textNode.textContent, titleType: 'title' };
}
return { title: null, titleType: null };
}
_toggleFavorite(title) {
const index = this.data.favorite.indexOf(title);
if (index >= 0) {
this.data.favorite.splice(index, 1);
} else {
this.data.favorite.push(title);
}
setValue('burglary', this.data);
}
}
const burglaryObserver = new BurglaryObserver();
async function checkBurglary(crimeType, data) {
if (crimeType !== '7') {
burglaryObserver.stop();
return;
}
burglaryObserver.onNewData(data);
}
const PP_CYCLING = 0;
const PP_DISTRACTED = 34; // eslint-disable-line no-unused-vars
const PP_MUSIC = 102;
const PP_LOITERING = 136;
const PP_PHONE = 170;
const PP_RUNNING = 204;
const PP_SOLICITING = 238; // eslint-disable-line no-unused-vars
const PP_STUMBLING = 272;
const PP_WALKING = 306;
const PP_BEGGING = 340;
const PP_SKINNY = 'Skinny';
const PP_AVERAGE = 'Average';
const PP_ATHLETIC = 'Athletic';
const PP_MUSCULAR = 'Muscular';
const PP_HEAVYSET = 'Heavyset';
const PP_ANY_BUILD = [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC, PP_MUSCULAR, PP_HEAVYSET];
const PP_MARKS = {
'Drunk Man': { level: 1, status: [PP_STUMBLING], build: PP_ANY_BUILD },
'Drunk Woman': { level: 1, status: [PP_STUMBLING], build: PP_ANY_BUILD },
'Homeless Person': { level: 1, status: [PP_BEGGING], build: [PP_AVERAGE] },
Junkie: { level: 1, status: [PP_STUMBLING], build: PP_ANY_BUILD },
'Elderly Man': { level: 1, status: [PP_WALKING], build: [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC, PP_HEAVYSET] },
'Elderly Woman': { level: 1, status: [PP_WALKING], build: [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC, PP_HEAVYSET] },
'Young Man': { level: 2, status: [PP_MUSIC], build: [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC] },
'Young Woman': { level: 2, status: [PP_PHONE], build: [PP_SKINNY, PP_AVERAGE, PP_HEAVYSET] },
Student: { level: 2, status: [PP_PHONE], build: [PP_SKINNY, PP_AVERAGE] },
'Classy Lady': {
level: 2,
status: [PP_PHONE, PP_WALKING],
build: [PP_SKINNY, PP_HEAVYSET],
bestBuild: [PP_HEAVYSET],
},
Laborer: { level: 2, status: [PP_PHONE], build: PP_ANY_BUILD },
'Postal Worker': { level: 2, status: [PP_WALKING], build: [PP_AVERAGE] },
'Rich Kid': {
level: 3,
status: [PP_WALKING, PP_PHONE],
build: [PP_SKINNY, PP_ATHLETIC, PP_HEAVYSET],
bestBuild: [PP_ATHLETIC],
},
'Sex Worker': { level: 3, status: [PP_PHONE], build: [PP_SKINNY, PP_AVERAGE], bestBuild: [PP_AVERAGE] },
Thug: { level: 3, status: [PP_RUNNING], build: [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC], bestBuild: [PP_SKINNY] },
Businessman: {
level: 4,
status: [PP_PHONE],
build: [PP_AVERAGE, PP_MUSCULAR, PP_HEAVYSET],
bestBuild: [PP_MUSCULAR, PP_HEAVYSET],
},
Businesswoman: {
level: 4,
status: [PP_PHONE],
build: [PP_SKINNY, PP_AVERAGE, PP_ATHLETIC],
bestBuild: [PP_ATHLETIC],
},
'Gang Member': {
level: 4,
status: [PP_LOITERING],
build: [PP_AVERAGE, PP_ATHLETIC, PP_MUSCULAR],
bestBuild: [PP_AVERAGE],
},
Jogger: { level: 4, status: [PP_WALKING], build: [PP_ATHLETIC, PP_MUSCULAR], bestBuild: [PP_MUSCULAR] },
Mobster: { level: 4, status: [PP_WALKING], build: [PP_SKINNY] },
Cyclist: { level: 5, status: [PP_CYCLING], build: ['1.52 m', `5'0"`, '1.62 m', `5'4"`] },
'Police Officer': {
level: 6,
status: [PP_RUNNING],
build: PP_ANY_BUILD,
bestBuild: [PP_SKINNY, '1.52 m', `5'0"`, '1.62 m', `5'4"`],
},
};
let pickpocketingOb = null;
let pickpocketingExitOb = null;
let pickpocketingInterval = 0;
async function checkPickpocketing(crimeType) {
if (crimeType !== '5') {
stopPickpocketing();
return;
}
const $wrapper = $('.pickpocketing-root');
if ($wrapper.length === 0) {
if (pickpocketingInterval === 0) {
// This is the first fetch.
pickpocketingInterval = setInterval(() => {
const $wrapperInInterval = $('.pickpocketing-root');
if ($wrapperInInterval.length === 0) {
return;
}
clearInterval(pickpocketingInterval);
pickpocketingInterval = 0;
startPickpocketing($wrapperInInterval);
}, 1000);
}
} else {
startPickpocketing($wrapper);
}
}
function refreshPickpocketing() {
const $wrapper = $('.pickpocketing-root');
const now = Date.now();
// Releasing reference to removed elements to avoid memory leak
pickpocketingExitOb.disconnect();
let isBelowExiting = false;
$wrapper.find('.crime-option').each(function () {
const $this = $(this);
const top = Math.floor($this.position().top);
const oldTop = parseInt($this.attr('data-cm-top'));
if (top !== oldTop) {
$this.attr('data-cm-top', top.toString());
$this.attr('data-cm-timestamp', now.toString());
}
const timestamp = parseInt($this.attr('data-cm-timestamp')) || now;
const isLocked = $this.is('[class*=locked___]');
const isExiting = $this.is('[class*=exitActive___]');
const isRecentlyMoved = now - timestamp <= 1000;
$this
.find('[class*=commitButtonSection___]')
.toggleClass('cm-overlay', !isLocked && (isBelowExiting || isRecentlyMoved))
.toggleClass('cm-overlay-fade', !isLocked && !isBelowExiting && isRecentlyMoved);
isBelowExiting = isBelowExiting || isExiting;
if (!$this.is('[class*=cm-pp-level-]')) {
const markAndTime = $this.find('[class*=titleAndProps___] > *:first-child').text().trim().toLowerCase();
const iconPosStr = $this.find('[class*=timerCircle___] [class*=icon___]').css('background-position-y');
const iconPosMatch = iconPosStr?.match(/(-?\d+)px/);
const iconPos = -parseInt(iconPosMatch?.[1] ?? '');
const build = $this.find('[class*=physicalPropsButton___]').text().trim().toLowerCase();
for (const [mark, markInfo] of Object.entries(PP_MARKS)) {
if (markAndTime.startsWith(mark.toLowerCase())) {
if (markInfo.status.includes(iconPos) && markInfo.build.some((b) => build.includes(b.toLowerCase()))) {
$this.addClass(`cm-pp-level-${markInfo.level}`);
if (markInfo.bestBuild?.some((b) => build.includes(b.toLowerCase()))) {
$this.addClass(`cm-pp-best-build`);
}
}
break;
}
}
}
pickpocketingExitOb.observe(this, { attributes: true, attributeFilter: ['class'], attributeOldValue: true });
});
}
function startPickpocketing($wrapper) {
if (!pickpocketingOb) {
pickpocketingOb = new MutationObserver(refreshPickpocketing);
pickpocketingExitOb = new MutationObserver(function (mutations) {
for (const mutation of mutations) {
if (
mutation.oldValue.indexOf('exitActive___') < 0 &&
mutation.target.className.indexOf('exitActive___') >= 0
) {
refreshPickpocketing();
return;
}
}
});
}
pickpocketingOb.observe($wrapper[0], {
childList: true,
characterData: true,
subtree: true,
});
}
function stopPickpocketing() {
if (!pickpocketingOb) {
return;
}
pickpocketingOb.disconnect();
pickpocketingOb = null;
pickpocketingExitOb.disconnect();
pickpocketingExitOb = null;
}
// Maximize extra exp (capitalization exp - total cost)
class ScammingSolver {
get BASE_ACTION_COST() {
return 0.02;
}
get FAILURE_COST_MAP() {
return this.algo === 'merit'
? {
1: 0,
20: 0,
40: 0,
60: 0,
80: 0,
}
: {
1: 1,
20: 1,
40: 1,
60: 0.5,
80: 0.33,
};
}
get CONCERN_SUCCESS_RATE() {
return 0.5;
}
get CELL_VALUE_MAP() {
return this.algo === 'merit'
? {
low: 2,
medium: 2,
high: 2,
fail: -20,
}
: {
low: 0.5,
medium: 1.5,
high: 2.5,
fail: -20, // The penalty should be -10. I add a bit to it for demoralization and chain bonus lost.
};
}
get SAFE_CELL_SET() {
return new Set(['neutral', 'low', 'medium', 'high', 'temptation']);
}
get DISPLACEMENT() {
// prettier-ignore
return {
1: {
strong: [[10, 19], [15, 29], [18, 35], [21, 39], [22, 42], [23, 44]],
soft: [[3, 7], [5, 11], [6, 13], [6, 14], [7, 15], [7, 16]],
back: [[-4, -2], [-6, -3], [-7, -4], [-8, -4], [-9, -4], [-9, -5]],
},
20: {
strong: [[8, 15], [12, 23], [15, 28], [16, 31], [18, 33], [18, 35]],
soft: [[3, 7], [5, 11], [6, 13], [6, 14], [7, 15], [7, 16]],
back: [[-4, -2], [-6, -3], [-7, -4], [-8, -4], [-9, -4], [-9, -5]],
},
40: {
strong: [[7, 13], [11, 20], [13, 24], [14, 27], [15, 29], [16, 30]],
soft: [[3, 6], [5, 9], [6, 11], [6, 12], [7, 13], [7, 14]],
back: [[-4, -2], [-6, -3], [-7, -4], [-8, -4], [-9, -4], [-9, -5]],
},
60: {
strong: [[6, 11], [9, 17], [11, 20], [12, 23], [13, 24], [14, 25]],
soft: [[2, 4], [3, 6], [4, 7], [4, 8], [4, 9], [5, 9]],
back: [[-4, -2], [-6, -3], [-7, -4], [-8, -4], [-9, -4], [-9, -5]],
},
80: {
strong: [[5, 9], [8, 14], [9, 17], [10, 19], [11, 20], [12, 21]],
soft: [[2, 3], [3, 5], [4, 6], [4, 6], [4, 7], [5, 7]],
back: [[-3, -2], [-5, -3], [-6, -4], [-6, -4], [-7, -4], [-7, -5]],
},
};
}
get MERIT_MASK_MAP() {
return {
temptation: 1n << 50n,
sensitivity: 1n << 51n,
hesitation: 1n << 52n,
concern: 1n << 53n,
};
}
get MERIT_REQUIREMENT_MASK() {
return 0xfn << 50n;
}
/**
* @param {'exp' | 'merit'} algo
* @param {('neutral' | 'low' | 'medium' | 'high' | 'temptation' | 'sensitivity' | 'hesitation' | 'concern' | 'fail')[]} bar
* @param {1 | 20 | 40 | 60 | 80} targetLevel
* @param {number} round
* @param {number} suspicion
*/
constructor(algo, bar, targetLevel, round, suspicion) {
this.algo = algo;
this.bar = bar;
this.targetLevel = targetLevel;
this.failureCost = this.FAILURE_COST_MAP[this.targetLevel];
this.initialRound = round;
this.initialSuspicion = suspicion;
this.driftArrayMap = new Map(); // (resolvingBitmap) => number[50]
this.dp = new Map(); // (resolvingBitmap | round) => {value: number, action: string, multi: number}[50]
this.resolvingMasks = new Array(50);
for (let pip = 0; pip < 50; pip++) {
if (this.resolvingMasks[pip]) {
continue;
}
if (this.bar[pip] !== 'hesitation' && this.bar[pip] !== 'concern') {
this.resolvingMasks[pip] = 0n;
continue;
}
let mask = this.algo === 'merit' ? this.MERIT_MASK_MAP[this.bar[pip]] : 0n;
for (let endPip = pip; endPip < 50 && this.bar[endPip] === this.bar[pip]; endPip++) {
mask += 1n << BigInt(endPip);
}
for (let endPip = pip; endPip < 50 && this.bar[endPip] === this.bar[pip]; endPip++) {
this.resolvingMasks[endPip] = mask;
}
}
}
/**
* @param {number} driftBitmap 1 for temptation triggered, 2 for sensitivity triggered
*/
solve(round, pip, resolvingBitmap, multiplierUsed, driftBitmap) {
if (this.algo === 'merit') {
for (let pip = 0; pip < 50; pip++) {
if (this._isResolved(pip, resolvingBitmap)) {
resolvingBitmap |= this.MERIT_MASK_MAP[this.bar[pip]] ?? 0n;
}
}
resolvingBitmap |= BigInt(driftBitmap) << 50n;
}
const result = this._visit(round - multiplierUsed, resolvingBitmap, multiplierUsed, pip);
return result[pip];
}
/**
* @param {number} round
* @param {bigint} resolvingBitmap
* @param {number} minMulti
* @param {number | undefined} singlePip
*/
_visit(round, resolvingBitmap, minMulti, singlePip = undefined) {
const dpKey = BigInt(round) | (resolvingBitmap << 6n);
const visited = this.dp.get(dpKey);
if (visited) {
return visited;
}
const result = new Array(50);
this.dp.set(dpKey, result);
if (this._estimateSuspicion(round) >= 50) {
for (let pip = 0; pip < 50; pip++) {
result[pip] = this._getCellResult(pip, resolvingBitmap);
}
return result;
}
const driftArray = this._getDriftArray(resolvingBitmap);
const [pipBegin, pipEnd] = singlePip !== undefined ? [singlePip, singlePip + 1] : [0, 50];
for (let pip = pipBegin; pip < pipEnd; pip++) {
const best = this._getCellResult(pip, resolvingBitmap);
if (this.bar[pip] === 'fail') {
result[pip] = best;
continue;
}
if (!this._isResolved(pip, resolvingBitmap)) {
if (this.bar[pip] === 'hesitation') {
const resolvedResult = this._visit(round, resolvingBitmap | this.resolvingMasks[pip], 0);
result[pip] = resolvedResult[pip];
continue;
}
if (this.bar[pip] === 'concern') {
const resolvedResult = this._visit(round + 1, resolvingBitmap | this.resolvingMasks[pip], 0);
const unresolvedResult = this._visit(round + 1, resolvingBitmap, 0);
const value =
resolvedResult[pip].value * this.CONCERN_SUCCESS_RATE +
(unresolvedResult[pip].value - this.failureCost) * (1 - this.CONCERN_SUCCESS_RATE) -
this.BASE_ACTION_COST;
result[pip] = {
value: Math.max(0, value),
action: value > 0 ? 'resolve' : 'abandon',
multi: 0,
};
continue;
}
}
for (let multi = minMulti; multi <= 5; multi++) {
const suspicionAfterMulti = this._estimateSuspicion(round + multi);
const nextRoundResult = this._visit(round + multi + 1, resolvingBitmap, 0);
for (const action of ['strong', 'soft', 'back']) {
const displacementArray = this.DISPLACEMENT[this.targetLevel.toString()]?.[action]?.[multi];
if (!displacementArray) {
continue;
}
const [minDisplacement, maxDisplacement] = displacementArray;
let totalValue = 0;
for (let disp = minDisplacement; disp <= maxDisplacement; disp++) {
const landingPip = Math.max(Math.min(pip + disp, 49), 0);
const newPip = driftArray[landingPip];
if (landingPip < suspicionAfterMulti || newPip < suspicionAfterMulti) {
totalValue += this.CELL_VALUE_MAP.fail;
} else {
if (!this.SAFE_CELL_SET.has(this.bar[landingPip]) && !this._isResolved(landingPip, resolvingBitmap)) {
totalValue -= this.failureCost;
}
totalValue -= this.BASE_ACTION_COST;
const landingResult =
this.algo === 'merit' && newPip !== landingPip
? this._visit(round + multi + 1, resolvingBitmap | this.MERIT_MASK_MAP[this.bar[landingPip]], 0)
: nextRoundResult;
totalValue += landingResult[newPip].value;
}
}
const avgValue = totalValue / (maxDisplacement - minDisplacement + 1) - this.BASE_ACTION_COST * multi;
if (avgValue > best.value) {
best.value = avgValue;
best.action = action;
best.multi = multi;
}
}
}
result[pip] = best;
}
return result;
}
_getDriftArray(resolvingBitmap) {
const cached = this.driftArrayMap.get(resolvingBitmap);
if (cached) {
return cached;
}
const driftArray = new Array(50);
this.driftArrayMap.set(resolvingBitmap, driftArray);
for (let pip = 0; pip < 50; pip++) {
let newPip = pip;
switch (this.bar[pip]) {
case 'temptation':
while (
newPip + 1 < 50 &&
(!this.SAFE_CELL_SET.has(this.bar[newPip]) || this.bar[newPip] === 'temptation') &&
!this._isResolved(newPip, resolvingBitmap)
) {
newPip++;
}
break;
case 'sensitivity':
while (newPip > 0 && this.bar[newPip] !== 'neutral' && !this._isResolved(newPip, resolvingBitmap)) {
newPip--;
}
break;
}
driftArray[pip] = newPip;
}
return driftArray;
}
_getCellResult(pip, resolvingBitmap) {
let value = this.CELL_VALUE_MAP[this.bar[pip]] ?? 0;
if (this.algo === 'merit' && (resolvingBitmap & this.MERIT_REQUIREMENT_MASK) !== this.MERIT_REQUIREMENT_MASK) {
value = Math.min(value, 0);
}
const action = this.bar[pip] === 'fail' ? 'fail' : value > 0 ? 'capitalize' : 'abandon';
return { value, action, multi: 0 };
}
_estimateSuspicion(round) {
if (round <= this.initialRound) {
return this.initialSuspicion;
}
const predefined = [0, 0, 0, 0, 2, 5, 8, 11, 16, 23, 34, 50][round] ?? 50;
const current = Math.floor(this.initialSuspicion * 1.5 ** (round - this.initialRound));
return Math.max(predefined, current);
}
_isResolved(pip, resolvingBitmap) {
return ((1n << BigInt(pip)) & resolvingBitmap) !== 0n;
}
}
class ScammingStore {
get TARGET_LEVEL_MAP() {
return {
'delivery scam': 1,
'family scam': 1,
'prize scam': 1,
'charity scam': 20,
'tech support scam': 20,
'vacation scam': 40,
'tax scam': 40,
'advance-fee scam': 60,
'job scam': 60,
'romance scam': 80,
'investment scam': 80,
};
}
get SPAM_ID_MAP() {
return {
295: 'delivery',
293: 'family',
291: 'prize',
297: 'charity',
299: 'tech support',
301: 'vacation',
303: 'tax',
305: 'advance-fee',
307: 'job',
309: 'romance',
311: 'investment',
};
}
constructor() {
this.data = getValue('scamming', {});
this.data.targets = this.data.targets ?? {};
this.data.farms = this.data.farms ?? {};
this.data.spams = this.data.spams ?? {};
this.data.defaultAlgo = this.data.defaultAlgo ?? 'exp';
this.unsyncedSet = new Set(Object.keys(this.data.targets));
this.solvers = {};
this.lastSolutions = {};
this.cash = undefined;
}
update(data) {
this._updateTargets(data.DB?.crimesByType?.targets);
this._updateFarms(data.DB?.additionalInfo?.currentOngoing);
this._updateSpams(data.DB?.currentUserStats?.crimesByIDAttempts, data.DB?.crimesByType?.methods);
this.cash = data.DB?.user?.money;
this._save();
}
setDefaultAlgo(algo) {
this.data.defaultAlgo = algo;
this._save();
}
changeAlgo(target) {
target.algos.push(target.algos.shift());
target.solution = null;
this._solve(target);
this._save();
}
_save() {
setValue('scamming', this.data);
}
_updateTargets(targets) {
if (!targets) {
return;
}
for (const target of targets) {
const stored = this.data.targets[target.subID];
if (stored && !target.new && target.bar) {
stored.driftBitmap = stored.driftBitmap ?? 0; // data migration for v1.4.6
let updated = false;
if (stored.multiplierUsed !== target.multiplierUsed || stored.pip !== target.pip) {
stored.multiplierUsed = target.multiplierUsed;
stored.pip = target.pip;
stored.expire = target.expire;
updated = true;
}
if (updated && this.unsyncedSet.has(stored.id)) {
stored.unsynced = true; // replied on another device
}
this.unsyncedSet.delete(stored.id);
if (stored.bar) {
for (let pip = 0; pip < 50; pip++) {
if (target.bar[pip] === stored.bar[pip]) {
continue;
}
if (target.bar[pip] === 'fail' && stored.suspicion <= pip) {
stored.suspicion = pip + 1;
updated = true;
}
if (target.bar[pip] === 'neutral' && (BigInt(stored.resolvingBitmap) & (1n << BigInt(pip))) === 0n) {
stored.resolvingBitmap = (BigInt(stored.resolvingBitmap) | (1n << BigInt(pip))).toString();
updated = true;
}
}
if (target.firstPip) {
if (stored.bar[target.firstPip] === 'temptation') {
stored.driftBitmap |= 1;
}
if (stored.bar[target.firstPip] === 'sensitivity') {
stored.driftBitmap |= 2;
}
}
}
if (updated) {
// Round is not accurate for concern and hesitation.
stored.round++;
}
if (!stored.bar) {
stored.bar = target.bar;
updated = true;
}
if (updated || !stored.solution) {
this._solve(stored);
}
} else {
const multiplierUsed = target.multiplierUsed ?? 0;
const pip = target.pip ?? 0;
const round = multiplierUsed === 0 && pip === 0 ? 0 : Math.max(1, multiplierUsed);
const stored = {
id: target.subID,
email: target.email,
level: this.TARGET_LEVEL_MAP[target.scamMethod.toLowerCase()] ?? 999,
round,
multiplierUsed,
pip,
expire: target.expire,
bar: target.bar ?? null,
suspicion: 0,
resolvingBitmap: '0',
driftBitmap: 0,
algos: null,
solution: null,
unsynced: round > 0,
};
this.data.targets[target.subID] = stored;
this._solve(stored);
}
}
const now = Math.floor(Date.now() / 1000);
for (const target of Object.values(this.data.targets)) {
if (target.expire < now) {
delete this.data.targets[target.id];
}
}
}
_updateFarms(currentOngoing) {
if (typeof currentOngoing !== 'object' || !(currentOngoing.length > 0)) {
return;
}
for (const item of currentOngoing) {
if (!item.type) {
continue;
}
this.data.farms[item.type] = { expire: item.timeEnded };
}
}
_updateSpams(crimesByIDAttempts, methods) {
if (!crimesByIDAttempts || !methods) {
return;
}
const now = Math.floor(Date.now() / 1000);
for (const [id, count] of Object.entries(crimesByIDAttempts)) {
const type = this.SPAM_ID_MAP[id];
const method = methods.find((x) => String(x.crimeID) === id);
if (!type || !method) {
continue;
}
const stored = this.data.spams[id];
if (stored) {
if (count !== stored.count) {
stored.count = count;
stored.accurate = now - stored.ts < 3600;
stored.since = now;
}
stored.ts = now;
stored.depreciation = method.depreciation;
} else {
this.data.spams[id] = {
count,
accurate: false,
since: null,
ts: now,
depreciation: method.depreciation,
};
}
}
}
_solve(target) {
if (!target.bar) {
return;
}
this.lastSolutions[target.id] = target.solution;
let solver = this.solvers[target.id];
if (!solver || solver.algo !== target.algos?.[0] || target.suspicion > 0) {
if (!target.algos) {
target.algos = this._isMeritFeasible(target) ? ['exp', 'merit'] : ['exp'];
const defaultIndex = target.algos.indexOf(this.data.defaultAlgo);
if (defaultIndex > 0) {
target.algos = [...target.algos.slice(defaultIndex), ...target.algos.slice(0, defaultIndex)];
}
}
solver = new ScammingSolver(target.algos[0], target.bar, target.level, target.round, target.suspicion);
this.solvers[target.id] = solver;
}
target.solution = solver.solve(
target.round,
target.pip,
BigInt(target.resolvingBitmap),
target.multiplierUsed,
target.driftBitmap,
);
}
_isMeritFeasible(target) {
const cells = new Set(target.bar);
return cells.has('temptation') && cells.has('sensitivity') && cells.has('hesitation') && cells.has('concern');
}
}
class ScammingObserver {
constructor() {
this.store = new ScammingStore();
this.crimeOptions = null;
this.farmIcons = null;
this.spamOptions = null;
this.virtualLists = null;
this.observer = new MutationObserver((mutations) => {
const isAdd = mutations.some((mutation) => {
for (const added of mutation.addedNodes) {
if (added instanceof HTMLElement) {
return true;
}
}
return false;
});
if (!isAdd) {
return;
}
for (const element of this.crimeOptions) {
if (!element.classList.contains('cm-sc-seen')) {
element.classList.add('cm-sc-seen');
this._refreshCrimeOption(element);
}
}
for (const element of this.farmIcons) {
if (!element.classList.contains('cm-sc-seen')) {
element.classList.add('cm-sc-seen');
this._refreshFarm(element);
}
}
for (const element of this.spamOptions) {
if (!element.classList.contains('cm-sc-seen')) {
element.classList.add('cm-sc-seen');
this._refreshSpam(element);
}
}
for (const element of this.virtualLists) {
if (!element.classList.contains('cm-sc-seen')) {
element.classList.add('cm-sc-seen');
this._refreshSettings(element);
}
}
});
}
start() {
if (this.crimeOptions) {
return;
}
this.crimeOptions = document.body.getElementsByClassName('crime-option');
this.farmIcons = document.body.getElementsByClassName('scraperPhisher___oy1Wn');
this.spamOptions = document.body.getElementsByClassName('optionWithLevelRequirement___cHH35');
this.virtualLists = document.body.getElementsByClassName('virtualList___noLef');
this.observer.observe($('.scamming-root')[0], { subtree: true, childList: true });
}
stop() {
this.crimeOptions = null;
this.observer.disconnect();
}
onNewData() {
this.start();
for (const element of this.crimeOptions) {
this._refreshCrimeOption(element);
}
for (const element of this.farmIcons) {
this._refreshFarm(element);
}
for (const element of this.spamOptions) {
this._refreshSpam(element);
}
}
_buildHintHtml(target, solution, lastSolution) {
const actionText =
{
strong: 'Fast Fwd',
soft: 'Soft Fwd',
back: 'Back',
capitalize: '$$$',
abandon: 'Abandon',
resolve: 'Resolve',
}[solution.action] ?? 'N/A';
const algoText =
{
exp: 'Exp',
merit: 'Merit',
}[target.algos?.[0]] ?? 'Score';
const score = Math.floor(solution.value * 100);
const scoreColor = score < 30 ? 't-red' : score < 100 ? 't-yellow' : 't-green';
const scoreDiff = lastSolution ? score - Math.floor(lastSolution.value * 100) : 0;
const scoreDiffColor = scoreDiff > 0 ? 't-green' : 't-red';
const scoreDiffText = scoreDiff !== 0 ? `(${scoreDiff > 0 ? '+' : ''}${scoreDiff})` : '';
let rspText = solution.multi > target.multiplierUsed ? 'Accel' : actionText;
let rspColor = '';
let fullRspText = solution.multi > 0 ? `(${target.multiplierUsed}/${solution.multi} + ${actionText})` : '';
if (target.unsynced) {
rspText = 'Unsynced';
rspColor = 't-gray-c';
fullRspText = fullRspText !== '' ? fullRspText : `(${actionText})`;
}
return `<span class="cm-sc-info cm-sc-hint cm-sc-hint-content">
<span><span class="cm-sc-algo">${algoText}</span>: <span class="${scoreColor}">${score}</span><span class="${scoreDiffColor}">${scoreDiffText}</span></span>
<span class="cm-sc-hint-action"><span class="${rspColor}">${rspText}</span> <span class="t-gray-c">${fullRspText}</span></span>
<span class="cm-sc-hint-button t-blue">Lv${target.level}</span>
</span>`;
}
_refreshCrimeOption(element) {
this._refreshTarget(element);
this._refreshFarmButton(element);
}
_refreshTarget(element) {
const $crimeOption = $(element);
const $email = $crimeOption.find('span.email___gVRXx');
const email = $email.text();
const target = Object.values(this.store.data.targets).find((x) => x.email === email);
if (!target) {
return;
}
// clear old info elements
const hasHint = $crimeOption.find('.cm-sc-hint-content').length > 0;
$crimeOption.find('.cm-sc-info').remove();
$email.parent().addClass('cm-sc-info-wrapper');
$email.parent().children().addClass('cm-sc-orig-info');
// hint
const solution = target.solution;
if (solution) {
if (!hasHint) {
$email.parent().removeClass('cm-sc-hint-hidden');
}
$crimeOption.attr('data-cm-action', solution.multi > target.multiplierUsed ? 'accelerate' : solution.action);
$crimeOption.toggleClass('cm-sc-unsynced', target.unsynced ?? false);
const lastSolution = this.store.lastSolutions[target.id];
$email.parent().append(this._buildHintHtml(target, solution, lastSolution));
$email.parent().append(`<span class="cm-sc-info cm-sc-orig-info cm-sc-hint-button t-blue">Hint</div>`);
$crimeOption.find('.cm-sc-hint-button').on('click', () => {
$email.parent().toggleClass('cm-sc-hint-hidden');
});
if (target.algos?.length > 1) {
const $algo = $crimeOption.find('.cm-sc-algo');
$algo.addClass('t-blue');
$algo.addClass('cm-sc-active');
$algo.on('click', () => {
this.store.changeAlgo(target);
this._refreshTarget(element);
});
}
} else {
$email.parent().addClass('cm-sc-hint-hidden');
}
// lifetime
const now = Math.floor(Date.now() / 1000);
const lifetime = formatLifetime(target.expire - now);
$email.before(`<span class="cm-sc-info ${lifetime.color}">${lifetime.text}</div>`);
// scale
const $cells = $crimeOption.find('.cell___AfwZm');
if ($cells.length >= 50) {
$cells.find('.cm-sc-scale').remove();
// Ignore cells after the first 50, which are faded out soon
for (let i = 0; i < 50; i++) {
const dist = i - target.pip;
const label = dist % 5 !== 0 || dist === 0 || dist < -5 ? '' : dist % 10 === 0 ? (dist / 10).toString() : "'";
let $scale = $cells.eq(i).children('.cm-sc-scale');
if ($scale.length === 0) {
$scale = $('<div class="cm-sc-scale"></div>');
$cells.eq(i).append($scale);
}
$scale.text(label);
}
}
// multiplier
const $accButton = $crimeOption.find('.response-type-button').eq(3);
$accButton.find('.cm-sc-multiplier').remove();
if (target.multiplierUsed > 0) {
$accButton.append(`<div class="cm-sc-multiplier">${target.multiplierUsed}</div>`);
}
}
_refreshFarmButton(element) {
const $element = $(element);
if ($element.find('.emailAddresses___ky_qG').length === 0) {
return;
}
$element.find('.commitButtonSection___wJfnI button').toggleClass('cm-sc-low-cash', this.store.cash < 10000);
}
_refreshFarm(element) {
const $element = $(element);
const label = $element.attr('aria-label') ?? '';
const farm = Object.entries(this.store.data.farms).find(([type]) => label.toLowerCase().includes(type))?.[1];
if (!farm) {
return;
}
const now = Math.floor(Date.now() / 1000);
const lifetime = formatLifetime(farm.expire - now);
$element.find('.cm-sc-farm-lifetime').remove();
$element.append(`<div class="cm-sc-farm-lifetime ${lifetime.color}">${lifetime.text}</div>`);
}
_refreshSpam(element) {
const $spamOption = $(element);
if ($spamOption.closest('.dropdownList').length === 0) {
return;
}
const label = $spamOption
.contents()
.filter((_, x) => x.nodeType === Node.TEXT_NODE)
.text();
const spam = Object.entries(this.store.data.spams).find(([id]) =>
label.toLowerCase().includes(this.store.SPAM_ID_MAP[id]),
)?.[1];
$spamOption.addClass('cm-sc-spam-option');
$spamOption.find('.cm-sc-spam-elapsed').remove();
if (!spam || !spam.since || spam.depreciation) {
return;
}
const now = Math.floor(Date.now() / 1000);
const elapsed = formatLifetime(now - spam.since);
if (!spam.accurate) {
elapsed.text = '> ' + elapsed.text;
}
if (elapsed.hours >= 24 * 8) {
elapsed.text = '> 7d';
}
$spamOption.append(`<div class="cm-sc-spam-elapsed ${elapsed.color}">${elapsed.text}</div>`);
}
_refreshSettings(element) {
const store = this.store;
const defaultAlgo = store.data.defaultAlgo;
const $settings = $(`<div class="cm-sc-settings">
<span>Default Strategy:</span>
<span class="cm-sc-algo-option t-blue" data-cm-value="exp">Exp</span>
<span class="cm-sc-algo-option t-blue" data-cm-value="merit">Merit</span>
</div>`);
$settings.children(`[data-cm-value="${defaultAlgo}"]`).addClass('cm-sc-active');
$settings.children('.cm-sc-algo-option').on('click', function () {
const $this = $(this);
store.setDefaultAlgo($this.attr('data-cm-value'));
$this.siblings().removeClass('cm-sc-active');
$this.addClass('cm-sc-active');
});
$settings.insertBefore(element);
}
}
const scammingObserver = new ScammingObserver();
async function checkScamming(crimeType, data) {
if (crimeType !== '12') {
scammingObserver.stop();
return;
}
scammingObserver.store.update(data);
scammingObserver.onNewData();
}
async function onCrimeData(crimeType, data) {
await checkDemoralization(data);
await checkBurglary(crimeType, data);
await checkPickpocketing(crimeType);
await checkScamming(crimeType, data);
}
function interceptFetch() {
const targetWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
const origFetch = targetWindow.fetch;
targetWindow.fetch = async (...args) => {
const rsp = await origFetch(...args);
try {
const url = new URL(args[0], location.origin);
const params = new URLSearchParams(url.search);
const reqBody = args[1]?.body;
const crimeType = params.get('typeID') ?? reqBody?.get('typeID');
if (url.pathname === '/page.php' && params.get('sid') === 'crimesData' && crimeType) {
const clonedRsp = rsp.clone();
await onCrimeData(crimeType, await clonedRsp.json());
}
} catch {
// ignore
}
return rsp;
};
}
function renderMorale() {
const interval = setInterval(async function () {
if (!$) {
return; // JQuery is not loaded in TornPDA yet
}
const $container = $('.crimes-app-header');
if ($container.length === 0) {
return;
}
clearInterval(interval);
$container.append(`<span>Morale: <span id="crime-morale-value">-</span>%</span>`);
const morale = parseInt(await getValue(STORAGE_MORALE));
if (!isNaN(morale)) {
updateMorale(morale);
}
// Show hidden debug button on double-click
let lastClick = 0; // dblclick event doesn't work well on mobile
$('#crime-morale-value')
.parent()
.on('click', function () {
if (Date.now() - lastClick > 1000) {
lastClick = Date.now();
return;
}
const data = {
morale: getValue(STORAGE_MORALE),
burglary: getValue('burglary'),
scamming: getValue('scamming'),
};
const export_uri = `data:application/json;charset=utf-8,${encodeURIComponent(JSON.stringify(data))}`;
$(this).replaceWith(`<a download="crime-morale-debug.json" href="${export_uri}"
class="torn-btn" style="display:inline-block;">Export Debug Data</a>`);
});
}, 500);
}
function updateMorale(morale) {
$('#crime-morale-value').text(morale.toString());
}
function renderStyle() {
addStyle(`
.cm-bg-lifetime {
position: absolute;
top: 0;
right: 0;
padding: 2px;
background: var(--default-bg-panel-color);
border: 1px solid darkgray;
}
.cm-bg-favor {
position: absolute;
right: 0;
bottom: 0;
background: #fffc;
height: 20px;
width: 20px;
font-size: 20px;
line-height: 1;
cursor: pointer;
pointer-events: auto !important;
}
.cm-bg-favor:after {
content: '\u2606';
display: block;
width: 100%;
height: 100%;
text-align: center;
}
.cm-bg-favor.cm-bg-active:after {
content: '\u2605';
color: orange;
}
:root {
--cm-pp-level-1: #37b24d;
--cm-pp-level-2: #95af14;
--cm-pp-level-3: #f4cc00;
--cm-pp-level-4: #fa9201;
--cm-pp-level-5: #e01111;
--cm-pp-level-6: #a016eb;
--cm-pp-filter-level-1: brightness(0) saturate(100%) invert(61%) sepia(11%) saturate(2432%) hue-rotate(79deg) brightness(91%) contrast(96%);
--cm-pp-filter-level-2: brightness(0) saturate(100%) invert(62%) sepia(80%) saturate(2102%) hue-rotate(32deg) brightness(99%) contrast(84%);
--cm-pp-filter-level-3: brightness(0) saturate(100%) invert(71%) sepia(53%) saturate(1820%) hue-rotate(9deg) brightness(107%) contrast(102%);
--cm-pp-filter-level-4: brightness(0) saturate(100%) invert(61%) sepia(62%) saturate(1582%) hue-rotate(356deg) brightness(94%) contrast(108%);
--cm-pp-filter-level-5: brightness(0) saturate(100%) invert(12%) sepia(72%) saturate(5597%) hue-rotate(354deg) brightness(105%) contrast(101%);
--cm-pp-filter-level-6: brightness(0) saturate(100%) invert(26%) sepia(84%) saturate(4389%) hue-rotate(271deg) brightness(86%) contrast(119%);
}
@keyframes cm-fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
visibility: hidden;
}
}
.cm-overlay {
position: relative;
}
.cm-overlay:after {
content: '';
position: absolute;
background: repeating-linear-gradient(135deg, #2223, #2223 70px, #0003 70px, #0003 80px);
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 900000;
}
.cm-overlay-fade:after {
animation-name: cm-fade-out;
animation-duration: 0.2s;
animation-timing-function: ease-in;
animation-fill-mode: forwards;
animation-delay: 0.4s
}
.cm-pp-level-1 {
color: var(--cm-pp-level-1);
}
.cm-pp-level-2 {
color: var(--cm-pp-level-2);
}
.cm-pp-level-3 {
color: var(--cm-pp-level-3);
}
.cm-pp-level-4 {
color: var(--cm-pp-level-4);
}
.cm-pp-level-5 {
color: var(--cm-pp-level-5);
}
.cm-pp-level-6 {
color: var(--cm-pp-level-6);
}
.cm-pp-level-1 [class*=timerCircle___] [class*=icon___] {
filter: var(--cm-pp-filter-level-1);
}
.cm-pp-level-2 [class*=timerCircle___] [class*=icon___] {
filter: var(--cm-pp-filter-level-2);
}
.cm-pp-level-3 [class*=timerCircle___] [class*=icon___] {
filter: var(--cm-pp-filter-level-3);
}
.cm-pp-level-4 [class*=timerCircle___] [class*=icon___] {
filter: var(--cm-pp-filter-level-4);
}
.cm-pp-level-5 [class*=timerCircle___] [class*=icon___] {
filter: var(--cm-pp-filter-level-5);
}
.cm-pp-level-6 [class*=timerCircle___] [class*=icon___] {
filter: var(--cm-pp-filter-level-6);
}
.cm-pp-level-1 [class*=timerCircle___] .CircularProgressbar-path {
stroke: var(--cm-pp-level-1) !important;
}
.cm-pp-level-2 [class*=timerCircle___] .CircularProgressbar-path {
stroke: var(--cm-pp-level-2) !important;
}
.cm-pp-level-3 [class*=timerCircle___] .CircularProgressbar-path {
stroke: var(--cm-pp-level-3) !important;
}
.cm-pp-level-4 [class*=timerCircle___] .CircularProgressbar-path {
stroke: var(--cm-pp-level-4) !important;
}
.cm-pp-level-5 [class*=timerCircle___] .CircularProgressbar-path {
stroke: var(--cm-pp-level-5) !important;
}
.cm-pp-level-6 [class*=timerCircle___] .CircularProgressbar-path {
stroke: var(--cm-pp-level-6) !important;
}
.cm-pp-level-1 [class*=commitButton___] {
border: 2px solid var(--cm-pp-level-1);
}
.cm-pp-level-2 [class*=commitButton___] {
border: 2px solid var(--cm-pp-level-2);
}
.cm-pp-level-3 [class*=commitButton___] {
border: 2px solid var(--cm-pp-level-3);
}
.cm-pp-level-4 [class*=commitButton___] {
border: 2px solid var(--cm-pp-level-4);
}
.cm-pp-level-5 [class*=commitButton___] {
border: 2px solid var(--cm-pp-level-5);
}
.cm-pp-best-build:not(.crime-option-locked) [class*=physicalPropsButton___]:before {
content: '\u2713 ';
font-weight: bold;
color: var(--cm-pp-level-2);
}
.cm-sc-info {
transform: translateY(1px);
}
.cm-sc-hint-button {
cursor: pointer;
}
.cm-sc-info-wrapper.cm-sc-hint-hidden > .cm-sc-hint,
.cm-sc-info-wrapper:not(.cm-sc-hint-hidden) > .cm-sc-orig-info {
display: none;
}
.cm-sc-hint-content {
display: flex;
justify-content: space-between;
flex-grow: 1;
gap: 5px;
white-space: nowrap;
overflow: hidden;
}
.cm-sc-hint-action {
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.cm-sc-seen[data-cm-action=strong] .response-type-button:nth-child(1):after,
.cm-sc-seen[data-cm-action=soft] .response-type-button:nth-child(2):after,
.cm-sc-seen[data-cm-action=back] .response-type-button:nth-child(3):after,
.cm-sc-seen[data-cm-action=accelerate] .response-type-button:nth-child(4):after,
.cm-sc-seen[data-cm-action=capitalize] .response-type-button:nth-child(5):after {
content: '\u2713';
color: var(--crimes-green-color);
position: absolute;
top: 0;
right: 0;
font-size: 12px;
font-weight: bolder;
line-height: 1;
z-index: 999;
}
.cm-sc-seen.cm-sc-unsynced[data-cm-action=strong] .response-type-button:nth-child(1):after,
.cm-sc-seen.cm-sc-unsynced[data-cm-action=soft] .response-type-button:nth-child(2):after,
.cm-sc-seen.cm-sc-unsynced[data-cm-action=back] .response-type-button:nth-child(3):after,
.cm-sc-seen.cm-sc-unsynced[data-cm-action=accelerate] .response-type-button:nth-child(4):after,
.cm-sc-seen.cm-sc-unsynced[data-cm-action=capitalize] .response-type-button:nth-child(5):after {
content: '?';
}
.cm-sc-seen[data-cm-action=abandon] .response-type-button:after {
content: '\u2715';
color: var(--crimes-stats-criticalFails-color);
position: absolute;
top: 0;
right: 0;
font-size: 12px;
font-weight: bolder;
line-height: 1;
z-index: 999;
}
.cm-sc-scale {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: calc(100% + 10px);
line-height: 1;
font-size: 8px;
display: flex;
align-items: flex-end;
justify-content: center;
}
.cm-sc-multiplier {
position: absolute;
bottom: 0;
right: 0;
text-align: right;
font-size: 10px;
line-height: 1;
}
.cm-sc-farm-lifetime {
padding-top: 2px;
text-align: center;
}
.cm-sc-spam-option .levelLabel___LNbg8,
.cm-sc-spam-option .separator___C2skk {
display: none;
}
.cm-sc-spam-elapsed {
position: absolute;
right: -5px;
}
.cm-sc-settings {
height: 40px;
width: 100%;
background: var(--default-bg-panel-color););
border-bottom: 1px solid var(--crimes-crimeOption-borderBottomColor);
padding-left: 10px;
box-sizing: border-box;
display: flex;
align-items: center;
gap: 20px;
}
.cm-sc-algo-option {
cursor: pointer;
line-height: 1.5;
border-top: 2px solid #0000;
border-bottom: 2px solid #0000;
}
.cm-sc-algo-option.cm-sc-active {
border-bottom-color: var(--default-blue-color);
}
.cm-sc-algo.cm-sc-active {
cursor: pointer;
}
.cm-sc-algo.cm-sc-active:before {
content: '\u21bb ';
}
.cm-sc-low-cash:after {
content: 'Low Cash';
color: var(--default-red-color);
position: absolute;
width: 100%;
left: 0;
top: calc(100% - 4px);
line-height: 1;
font-size: 12px;
}
`);
}
interceptFetch();
renderMorale();
if (document.readyState === 'loading') {
document.addEventListener('readystatechange', () => {
if (document.readyState === 'interactive') {
renderStyle();
}
});
} else {
renderStyle();
}
})();