TORN: Display Crime Chain

Calculates and displays your current crime chain

目前为 2023-12-09 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name TORN: Display Crime Chain
  3. // @namespace http://torn.city.com.dot.com.com
  4. // @version 1.0.0
  5. // @description Calculates and displays your current crime chain
  6. // @author Ironhydedragon[2428902]
  7. // @match https://www.torn.com/loader.php?sid=crimes*
  8. // @license MIT
  9. // @run-at document-end
  10. // ==/UserScript==
  11.  
  12. let crimeChain = 0;
  13.  
  14. const redFlame = '#e64d1a';
  15.  
  16. const PDA_API_KEY = '###PDA-APIKEY###';
  17. function isPDA() {
  18. const PDATestRegex = !/^(###).+(###)$/.test(PDA_API_KEY);
  19. console.log('REGEX', PDATestRegex); // TEST
  20. return PDATestRegex;
  21. }
  22.  
  23. function setApiKey(apiKey) {
  24. localStorage.setItem('ihdScriptApiKey', apiKey);
  25. }
  26. function getApiKey() {
  27. return localStorage.getItem('ihdScriptApiKey');
  28. }
  29.  
  30. const stylesheet = `
  31. <style>
  32. #crime-chain {
  33. cursor: unset;
  34. }
  35.  
  36. #api-form.header-wrapper-top {
  37. display: flex;
  38. }
  39. #api-form.header-wrapper-top .container {
  40. display: flex;
  41. justify-content: start;
  42. align-items: center;
  43. padding-left: 20px;
  44. }
  45.  
  46. #api-form.header-wrapper-top h2 {
  47. display: block;
  48. text-align: center;
  49. margin: 0;
  50. width: 172px;
  51. }
  52.  
  53. #api-form.header-wrapper-top input {
  54. background: linear-gradient(0deg, #111, #000);
  55. border-radius: 5px;
  56. box-shadow: 0 1px 0 hsla(0, 0%, 100%, 0.102);
  57. box-sizing: border-box;
  58. color: #9f9f9f;
  59. display: inline;
  60. font-weight: 400;
  61. height: 24px;
  62. width: clamp(170px, 50%, 250px);
  63. margin: 0 0 0 21px;
  64. outline: none;
  65. padding: 0 10px 0 10px;
  66.  
  67. font-size: 12px;
  68. font-style: italic;
  69. vertical-align: middle;
  70. border: 0;
  71. text-shadow: none;
  72. z-index: 100;
  73. }
  74. #api-form.header-wrapper-top a {
  75. margin: 0 8px;
  76. }
  77.  
  78. @media screen and (max-width: 1000px) {
  79. #api-form.header-wrapper-top h2 {
  80. width: 148px;
  81. }
  82. #api-form.header-wrapper-top input {
  83. margin-left: 10px;
  84. }
  85. }
  86. @media screen and (max-width: 784px) {
  87. #api-form.header-wrapper-top h2 {
  88. font-size: 16px;
  89. width: 80px;
  90. }
  91.  
  92. #crime-chain .linkTitle____NPyM {
  93. display: block;
  94. }
  95. #body.r .linksContainer___LiOTN {
  96. margin-left: 8px;
  97. }
  98. }
  99.  
  100. </style>`;
  101. function renderStylesheet() {
  102. document.head.insertAdjacentHTML('beforeend', stylesheet);
  103. }
  104.  
  105. function renderApiForm() {
  106. const topHeaderBannerEl = document.querySelector('#topHeaderBanner');
  107. const apiFormHTML = `
  108. <div id="api-form" class="header-wrapper-top">
  109. <div class="container clear-fix">
  110. <h2>API Key</h2>
  111. <input
  112. id="api-form__input"
  113. type="text"
  114. placeholder="Enter a full-acces API key..."
  115. />
  116. <a href="#" id="api-form__submit" type="btn" disabled><span class="link-text">Submit</span</button>
  117. </div>
  118. </div>`;
  119.  
  120. topHeaderBannerEl.insertAdjacentHTML('afterbegin', apiFormHTML);
  121. }
  122. function dismountApiForm() {
  123. document.querySelector('#api-form').remove();
  124. }
  125.  
  126. function renderCrimeChainHTML() {
  127. console.log('🖼️ RENDERING CHAIN HTML'); // TEST
  128. const crimeChainHTML = `
  129. <div class="linksContainer___LiOTN">
  130. <span aria-labelledby="crime-chain" class="linkContainer___X16y4 inRow___VfDnd greyLineV___up8VP link-container-CrimesHub" target="_self" id="crime-chain"
  131. ><span class="iconContainer___D5z6F linkIconContainer___Ep0LO"
  132. ><svg fill="#777777" height="17px" width="16px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 31.891 31.891" xml:space="preserve">
  133. <g id="SVGRepo_bgCarrier" stroke-width="0"></g>
  134. <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
  135. <g id="SVGRepo_iconCarrier">
  136. <g>
  137. <path
  138. d="M30.543,5.74l-4.078-4.035c-1.805-1.777-4.736-1.789-6.545-0.02l-4.525,4.414c-1.812,1.768-1.82,4.648-0.02,6.424 l2.586-2.484c-0.262-0.791,0.061-1.697,0.701-2.324l2.879-2.807c0.912-0.885,2.375-0.881,3.275,0.01l2.449,2.42 c0.9,0.891,0.896,2.326-0.01,3.213l-2.879,2.809c-0.609,0.594-1.609,0.92-2.385,0.711l-2.533,2.486 c1.803,1.781,4.732,1.789,6.545,0.02l4.52-4.41C32.34,10.396,32.346,7.519,30.543,5.74z"
  139. ></path>
  140. <path
  141. d="M13.975,21.894c0.215,0.773-0.129,1.773-0.752,2.381l-2.689,2.627c-0.922,0.9-2.414,0.895-3.332-0.012l-2.498-2.461 c-0.916-0.906-0.91-2.379,0.012-3.275l2.691-2.627c0.656-0.637,1.598-0.961,2.42-0.689l2.594-2.57 c-1.836-1.811-4.824-1.82-6.668-0.02l-4.363,4.26c-1.846,1.803-1.855,4.734-0.02,6.549l4.154,4.107 c1.834,1.809,4.82,1.818,6.668,0.018l4.363-4.26c1.844-1.805,1.852-4.734,0.02-6.547L13.975,21.894z"
  142. ></path>
  143. <path d="M11.139,20.722c0.611,0.617,1.611,0.623,2.234,0.008l7.455-7.416c0.621-0.617,0.625-1.615,0.008-2.234 c-0.613-0.615-1.611-0.619-2.23-0.006l-7.457,7.414C10.529,19.103,10.525,20.101,11.139,20.722z"></path>
  144. </g>
  145. </g></svg></span
  146. ><span class="linkTitle____NPyM"><span id="crime-chain__current">###</span></span></span
  147. >
  148. </div>
  149. `;
  150. const titleContainerEl = document.querySelector('.crimes-app .titleContainer___QrlWP');
  151. titleContainerEl.insertAdjacentHTML('afterend', crimeChainHTML);
  152. }
  153.  
  154. function renderCrimeChainCurrent() {
  155. console.log('⛓️', crimeChain); // TEST
  156. document.querySelector('#crime-chain__current').textContent = Math.floor(crimeChain);
  157. }
  158.  
  159. async function fetchCrimes(toTimestamp) {
  160. const response = await fetch(`https://api.torn.com/user/?selections=log&cat=136${toTimestamp ? '&to=' + toTimestamp : ''}&key=${getApiKey()}`);
  161. const data = await response.json();
  162. return data;
  163. }
  164.  
  165. async function calcCrimeChain() {
  166. try {
  167. let dataCollector = [];
  168.  
  169. const initialData = await fetchCrimes();
  170. function dataCollectorUnshifter(fetchData) {
  171. for (const log in fetchData.log) {
  172. if (fetchData.log[log].title.match(/Crime (success|fail|critical fail)/gi)) {
  173. dataCollector.unshift(fetchData.log[log]);
  174. }
  175. }
  176. }
  177. dataCollectorUnshifter(initialData);
  178. while (dataCollector.filter((log) => log.title.match(/Crime critical fail/i)).length < 1) {
  179. const data = await fetchCrimes(dataCollector[0].timestamp - 1);
  180. dataCollectorUnshifter(data);
  181. }
  182.  
  183. for (const d of dataCollector) {
  184. if (d.title.match(/Crime success/i)) {
  185. crimeChain++;
  186. }
  187. if (d.title.match(/Crime fail/i)) {
  188. crimeChain = crimeChain ? crimeChain / 2 : 0;
  189. }
  190. if (d.title.match(/Crime critical fail/i)) {
  191. crimeChain = 0;
  192. }
  193. }
  194. } catch (error) {
  195. console.error(error); // TEST
  196. }
  197. }
  198.  
  199. //// Callbacks
  200. function submitFormCallback() {
  201. const inputEl = document.querySelector('#api-form__input');
  202. const submitBtnEl = document.querySelector('#api-form__submit');
  203.  
  204. const apiKey = inputEl.value;
  205. if (apiKey.length !== 16) {
  206. inputEl.style.border = `2px solid ${redFlame}`;
  207. submitBtnEl.disabled = true;
  208. return;
  209. }
  210. setApiKey(apiKey);
  211. dismountApiForm();
  212. window.location.reload();
  213. }
  214.  
  215. function inputValidatorCallback(event) {
  216. const inputEl = document.querySelector('#api-form__input');
  217. const submitBtnEl = document.querySelector('#api-form__submit');
  218. if (event.target.value.length === 16) {
  219. submitBtnEl.disabled = false;
  220. inputEl.style.border = '1px solid #444';
  221. }
  222. if (event.target.value.length !== 16) {
  223. submitBtnEl.disabled = true;
  224. }
  225. }
  226.  
  227. function updateCrimeCallback(mutationList) {
  228. for (const mutation of mutationList) {
  229. if (mutation.addedNodes.length > 0 && mutation.addedNodes[0].classList && [...mutation.addedNodes[0].classList].join(' ').match(/crimes-outcome-/)) {
  230. const outcome = [...mutation.addedNodes[0].classList].join(' ').match(/(?<=crimes-outcome-)\w+/gi)[0];
  231. console.log('👀', outcome); // TEST
  232.  
  233. if (outcome === 'success') {
  234. crimeChain++;
  235. }
  236. if (outcome === 'failure') {
  237. crimeChain = crimeChain / 2;
  238. }
  239. if (outcome === 'criticalFailure') {
  240. crimeChain = crimeChain / 2;
  241. }
  242.  
  243. renderCrimeChainCurrent();
  244. }
  245. }
  246. }
  247.  
  248. //////// CONTROLLERS ////////
  249. function apiKeyFormController() {
  250. renderApiForm();
  251.  
  252. // set event liseners
  253. //// Event listeners
  254. document.querySelector('#api-form__submit').addEventListener('click', submitFormCallback);
  255. document.querySelector('#api-form__input').addEventListener('input', inputValidatorCallback);
  256. document.querySelector('#api-form__input').addEventListener('keyup', (event) => {
  257. if (event.key === 'Enter' || event.keyCode === 13) {
  258. submitFormCallback();
  259. }
  260. });
  261. return;
  262. }
  263.  
  264. function initController() {
  265. renderStylesheet();
  266.  
  267. if (isPDA()) {
  268. console.log('🌟 IS PDA!!!!!', PDA_API_KEY); // TEST
  269. setApiKey(PDA_API_KEY);
  270. }
  271.  
  272. if (!getApiKey()) {
  273. console.log('noAPIKey found', getApiKey()); // TEST
  274. apiKeyFormController();
  275. return;
  276. }
  277.  
  278. renderCrimeChainHTML();
  279. }
  280.  
  281. async function loadController() {
  282. await calcCrimeChain();
  283. renderCrimeChainCurrent();
  284. }
  285.  
  286. function updateCrimeChainController() {
  287. const updateCrimeObserver = new MutationObserver(updateCrimeCallback);
  288. updateCrimeObserver.observe(document, { attributes: false, childList: true, subtree: true });
  289. }
  290.  
  291. //// Promise race conditions
  292. // necessary as PDA scripts are inject after window.onload
  293. const PDAPromise = new Promise((res, rej) => {
  294. if (document.readyState === 'complete') res();
  295. });
  296.  
  297. const browserPromise = new Promise((res, rej) => {
  298. window.addEventListener('load', () => res());
  299. });
  300.  
  301. (async () => {
  302. try {
  303. console.log('⛓️ Crime chain script ON!'); // TEST
  304. await Promise.race([PDAPromise, browserPromise]);
  305. initController();
  306. if (getApiKey()) {
  307. await loadController();
  308. updateCrimeChainController();
  309. }
  310. } catch (error) {
  311. console.error(error); // TEST
  312. }
  313. })();