TORN: Display Crime Chain

Calculates and displays your current crime chain

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         TORN: Display Crime Chain
// @namespace    http://torn.city.com.dot.com.com
// @version      1.0.5
// @description  Calculates and displays your current crime chain
// @author       Ironhydedragon[2428902]
// @match        https://www.torn.com/page.php?sid=crimes*
// @license      MIT
// @run-at       document-end
// ==/UserScript==

let crimeChain = 0;

const redFlame = '#e64d1a';

const PDA_API_KEY = '###PDA-APIKEY###';
function isPDA() {
  const PDATestRegex = !/^(###).+(###)$/.test(PDA_API_KEY);
  console.log('REGEX', PDATestRegex); // TEST
  return PDATestRegex;
}

function setApiKey(apiKey) {
  localStorage.setItem('ihdScriptApiKey', apiKey);
}
function getApiKey() {
  return localStorage.getItem('ihdScriptApiKey');
}

const stylesheet = `
  <style>
  #crime-chain {
    cursor: unset;
  }

  #api-form.header-wrapper-top {
    display: flex;
  }
  #api-form.header-wrapper-top .container {
    display: flex;
    justify-content: start;
    align-items: center;
    padding-left: 20px;
  }

  #api-form.header-wrapper-top h2 {
    display: block;
    text-align: center;
    margin: 0;
    width: 172px;
  }

  #api-form.header-wrapper-top input {
    background: linear-gradient(0deg, #111, #000);
    border-radius: 5px;
    box-shadow: 0 1px 0 hsla(0, 0%, 100%, 0.102);
    box-sizing: border-box;
    color: #9f9f9f;
    display: inline;
    font-weight: 400;
    height: 24px;
    width: clamp(170px, 50%, 250px);
    margin: 0 0 0 21px;
    outline: none;
    padding: 0 10px 0 10px;

    font-size: 12px;
    font-style: italic;
    vertical-align: middle;
    border: 0;
    text-shadow: none;
    z-index: 100;
  }
  #api-form.header-wrapper-top a {
    margin: 0 8px;
  }

  @media screen and (max-width: 1000px) {
    #api-form.header-wrapper-top h2 {
      width: 148px;
    }
    #api-form.header-wrapper-top input {
      margin-left: 10px;
    }
  }
  @media screen and (max-width: 784px) {
    #api-form.header-wrapper-top h2 {
      font-size: 16px;
      width: 80px;
    }

    #crime-chain .linkTitle____NPyM {
      display: block;
    }
    #body.r .linksContainer___LiOTN {
      margin-left: 8px;
    }
  }

  </style>`;
function renderStylesheet() {
  document.head.insertAdjacentHTML('beforeend', stylesheet);
}

function renderApiForm() {
  const topHeaderBannerEl = document.querySelector('#topHeaderBanner');
  const apiFormHTML = `
      <div id="api-form" class="header-wrapper-top">
        <div class="container clear-fix"> 
          <h2>API Key</h2>
          <input
            id="api-form__input"
            type="text"
            placeholder="Enter a full-acces API key..."
          />
          <a href="#" id="api-form__submit"  type="btn" disabled><span class="link-text">Submit</span</button>
        </div>
      </div>`;
  if (document.querySelector('#api-form')) return;
  topHeaderBannerEl.insertAdjacentHTML('afterbegin', apiFormHTML);
}
function dismountApiForm() {
  document.querySelector('#api-form').remove();
}

function renderCrimeChainHTML() {
  console.log('🖼️ RENDERING CHAIN HTML'); // TEST
  const crimeChainHTML = `
    <div class="linksContainer___LiOTN">
      <span aria-labelledby="crime-chain" class="linkContainer___X16y4 inRow___VfDnd greyLineV___up8VP link-container-CrimesHub" target="_self" id="crime-chain"
        ><span class="iconContainer___D5z6F linkIconContainer___Ep0LO"
          ><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">
            <g id="SVGRepo_bgCarrier" stroke-width="0"></g>
            <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
            <g id="SVGRepo_iconCarrier">
              <g>
                <path
                  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"
                ></path>
                <path
                  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"
                ></path>
                <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>
              </g>
            </g></svg></span
        ><span class="linkTitle____NPyM"><span aria-label="current crime chain" id="crime-chain__current">###</span></span></span
      >
    </div>
    `;
  const titleContainerEl = document.querySelector('.crimes-app .heading___dOsMq');
  // const titleContainerEl = document.querySelector('.crimes-app .titleContainer___QrlWP');
  if (document.querySelector('#crime-chain')) return;
  titleContainerEl.insertAdjacentHTML('afterend', crimeChainHTML);
}

function renderCrimeChainCurrent() {
  console.log('⛓️', crimeChain); // TEST
  document.querySelector('#crime-chain__current').textContent = Math.floor(crimeChain);
}

async function fetchCrimes(toTimestamp) {
  const response = await fetch(`https://api.torn.com/user/?selections=log&cat=136${toTimestamp ? '&to=' + toTimestamp : ''}&key=${getApiKey()}`);
  const data = await response.json();
  return data;
}

async function calcCrimeChain() {
  try {
    let dataCollector = [];

    const initialData = await fetchCrimes();
    function dataCollectorUnshifter(fetchData) {
      for (const log in fetchData.log) {
        if (fetchData.log[log].title.match(/Crime (success|fail|critical fail)/gi)) {
          dataCollector.unshift(fetchData.log[log]);
        }
      }
    }
    dataCollectorUnshifter(initialData);
    while (dataCollector.filter((log) => log.title.match(/Crime critical fail/i)).length < 1) {
      const data = await fetchCrimes(dataCollector[0].timestamp - 1);
      dataCollectorUnshifter(data);
    }

    for (const d of dataCollector) {
      if (d.title.match(/Crime success/i)) {
        crimeChain++;
      }
      if (d.title.match(/Crime fail/i)) {
        crimeChain = crimeChain ? crimeChain / 2 : 0;
      }
      if (d.title.match(/Crime critical fail/i)) {
        crimeChain = 0;
      }
    }
  } catch (error) {
    console.error(error); // TEST
  }
}

//// Callbacks
function submitFormCallback() {
  const inputEl = document.querySelector('#api-form__input');
  const submitBtnEl = document.querySelector('#api-form__submit');

  const apiKey = inputEl.value;
  if (apiKey.length !== 16) {
    inputEl.style.border = `2px solid ${redFlame}`;
    submitBtnEl.disabled = true;
    return;
  }
  setApiKey(apiKey);
  dismountApiForm();
  window.location.reload();
}

function inputValidatorCallback(event) {
  const inputEl = document.querySelector('#api-form__input');
  const submitBtnEl = document.querySelector('#api-form__submit');
  if (event.target.value.length === 16) {
    submitBtnEl.disabled = false;
    inputEl.style.border = '1px solid #444';
  }
  if (event.target.value.length !== 16) {
    submitBtnEl.disabled = true;
  }
}

function updateCrimeCallback(mutationList) {
  for (const mutation of mutationList) {
    if (mutation.addedNodes.length > 0 && mutation.addedNodes[0].classList && [...mutation.addedNodes[0].classList].join(' ').match(/crimes-outcome-/)) {
      const outcome = [...mutation.addedNodes[0].classList].join(' ').match(/(?<=crimes-outcome-)\w+/gi)[0];
      console.log('👀', outcome); // TEST

      if (outcome === 'success') {
        crimeChain++;
      }
      if (outcome === 'failure') {
        crimeChain = crimeChain / 2;
      }
      if (outcome === 'criticalFailure') {
        crimeChain = crimeChain / 2;
      }

      renderCrimeChainCurrent();
    }
  }
}

//////// CONTROLLERS ////////
function apiKeyFormController() {
  renderApiForm();

  // set event liseners
  //// Event listeners
  document.querySelector('#api-form__submit').addEventListener('click', submitFormCallback);
  document.querySelector('#api-form__input').addEventListener('input', inputValidatorCallback);
  document.querySelector('#api-form__input').addEventListener('keyup', (event) => {
    if (event.key === 'Enter' || event.keyCode === 13) {
      submitFormCallback();
    }
  });
  return;
}

function initController() {
  renderStylesheet();

  if (isPDA()) {
    console.log('🌟 IS PDA!!!!!', PDA_API_KEY); // TEST
    setApiKey(PDA_API_KEY);
  }

  if (!getApiKey()) {
    console.log('noAPIKey found'); // TEST
    apiKeyFormController();
    return;
  }

  renderCrimeChainHTML();
}

async function loadController() {
  await calcCrimeChain();
  renderCrimeChainCurrent();
}

function updateCrimeChainController() {
  const updateCrimeObserver = new MutationObserver(updateCrimeCallback);
  updateCrimeObserver.observe(document, { attributes: false, childList: true, subtree: true });
}

//// Promise race conditions
// necessary as PDA scripts are inject after window.onload
const PDAPromise = new Promise((res, rej) => {
  if (document.readyState === 'complete') res();
});

const browserPromise = new Promise((res, rej) => {
  window.addEventListener('load', () => res());
});

(async () => {
  try {
    console.log('⛓️ Crime chain script ON!'); // TEST
    await Promise.race([PDAPromise, browserPromise]);
    initController();
    if (getApiKey()) {
      await loadController();
      updateCrimeChainController();
    }
  } catch (error) {
    console.error(error); // TEST
  }
})();