TORN: Display Weapon Bonus in Auction House + Highlight

Displays weapon bonuses + stats next to weapon name in auction house

// ==UserScript==
// @name         TORN: Display Weapon Bonus in Auction House + Highlight
// @namespace    http://torn.city.com.dot.com.com
// @version      1.1.1
// @description  Displays weapon bonuses + stats next to weapon name in auction house
// @author       Ironhydedragon
// @match        https://www.torn.com/amarket.php*
// @license      MIT
// @run-at       document-end
// ==/UserScript==

//////// GLOBAL VARIABLES ////////
const GLOBAL_STATE = {
  userSettings: {
    highlightWeaponColors: true, // POSSIBLE VALUES: true or false. Change value to turn on or off color highlighting
  },
};

////  Colors
const greenMossDark = '#4b5738';
const greenMossDarkTranslucent = 'rgb(75, 87, 56, 0.9)';
const greenMoss = '#57693a';
const greenMossTranslucent = 'rgb(87, 105, 58, 0.9)';
const greenApple = '#85b200';
const greenAppleTranslucent = 'rgba(134, 179, 0, 0.4)';

const yellow = '#ff0';
const yellowTranslucent = 'rgba(255, 255, 0,0.4)';
const yellowIcterine = '#fcf75e';
const yellowIcterineTranslucent = 'rgb(252, 247, 94, 0.1)';

const orangeFulvous = '#d08000';
const orangeFulvousTranslucent = 'rgba(209, 129, 0, 0.2)';
const orangeAmber = '#ffbf00';
const orangeAmberTranslucent = 'rgba(255, 191, 0, 0.4)';

const redFlame = '#e64d1a';
const redFlameTranslucent = 'rgba(230, 77, 25, 0.2)';
const redMelon = '#ffa8a8';
const redMelonTranslucent = 'rgba(255, 168, 168, 0.3)';

//////// MODEL ////////
function getGlobalState() {
  return GLOBAL_STATE;
}
function getUserSettings() {
  return getGlobalState().userSettings;
}
function isHighlightingWeapons() {
  return getUserSettings().highlightWeaponColors;
}

//////// UTIL FUNCTIONS /////////
async function requireElement(selectors, conditionsCallback) {
  try {
    await new Promise((res, rej) => {
      maxCycles = 500;
      let current = 1;
      const interval = setInterval(() => {
        if (document.querySelector(selectors)) {
          if (conditionsCallback === undefined) {
            clearInterval(interval);
            return res();
          }
          if (conditionsCallback(document.querySelector(selectors))) {
            clearInterval(interval);
            return res();
          }
        }
        if (current === maxCycles) {
          clearInterval(interval);
          rej('Timeout: Could not find element on page');
        }
        current++;
      }, 10);
    });
  } catch (err) {
    console.error(err);
  }
}

function getAuctionListItems() {
  let listItemsArr = [...document.querySelectorAll('ul.items-list li')];
  const filterOutClasses = ['last', 'clear'];
  listItemsArr = listItemsArr.filter((li) => {
    return !filterOutClasses.some((c) => li.classList.contains(c));
  });
  return listItemsArr;
}

//// Callbacks
function auctionItemsLoadedCallback(queryEl) {
  const firstListItem = queryEl.querySelector('li');
  return !firstListItem.classList.contains('last');
}

async function auctionObserverCallback(mutationList, observer) {
  for (const mutation of mutationList) {
    if (mutation.target.classList.contains('items-list') && mutation.addedNodes.length > 10) {
      observer.disconnect();
      await loadController();
    }
  }
}

//// Observers
function createAuctionMutationObserver() {
  const observer = new MutationObserver(auctionObserverCallback);
  observer.observe(document, {
    attributes: false,
    childList: true,
    subtree: true,
  });
}

//////// VIEW ////////
const stylesheet = `
  <style>
    .display-bonus--red {
      --db-bgc: ${redFlameTranslucent};
      --db-outline: ${redFlame};
    }
    .display-bonus--orange {
      --db-bgc: ${orangeFulvousTranslucent};
      --db-outline: ${orangeFulvous};
    }
    .display-bonus--yellow {
      --db-bgc: ${yellowIcterineTranslucent};
      --db-outline: ${yellow};
    }
    .dark-mode .display-bonus--yellow {
      --db-bgc: ${yellowIcterineTranslucent};
      --db-outline: ${yellowTranslucent};
    }
  

    .display-bonus {
      background: var(--db-bgc);
      outline: 1px solid var(--db-outline);
      outline-offset: -2px;
    }

    .display-bonus__container {
      display: block
    }

    p.display-bonus__bonus {
      display: inline-block;
      padding-right: 4px;
    }

    .display-bonus .item-bonuses .iconsbonuses span:nth-of-type(1) i{
      transform: scale(2) translate(-2px, 2px);
    }
    .display-bonus .item-bonuses .iconsbonuses span:nth-of-type(2) i{
      transform: scale(2) translate(-6px, 2px);
    }
  </style>
`;
function renderStylesheet() {
  document.head.insertAdjacentHTML('beforeend', stylesheet);
}

function renderWeaponBonuses(weaponEl) {
  let rawBonusTextArr = [...weaponEl.querySelectorAll('.iconsbonuses span')].map((spanEl) => spanEl.title);

  const bonusString = rawBonusTextArr.map((raw) => {
    const name = raw.match(/(?<=<b>)\w+\s*-*\w*/);
    const bonus = raw.match(/\d+%|\d+\sturns/);

    return `<b>${name}</b> ${bonus || ''}`;
  });

  const titleContainerEl = weaponEl.querySelector('span.title');
  titleContainerEl.querySelector('p').remove();

  const bonusContainerHTML = `<div class="display-bonus__container"></div>`;
  titleContainerEl.insertAdjacentHTML('beforeend', bonusContainerHTML);

  for (const bonus of bonusString) {
    const bonusHTML = `<p class="t-gray-6 display-bonus__bonus">${bonus}</p>`;
    titleContainerEl.querySelector('.display-bonus__container').insertAdjacentHTML('beforeend', bonusHTML);
  }
}

function renderColorClass(weaponEl) {
  colorType = weaponEl.outerHTML.match(/(?<=glow-)\w+/);
  weaponEl.classList.add(`display-bonus`, `display-bonus--${colorType}`);
}

//////// CONTROLLERS ////////
async function initController() {
  console.log('🗡️ Display bonus script is running!'); // TEST
  renderStylesheet();
}

async function loadController() {
  try {
    await requireElement('ul.items-list', auctionItemsLoadedCallback);
    const listItemsArr = getAuctionListItems();

    for (const item of listItemsArr) {
      renderWeaponBonuses(item);
      if (isHighlightingWeapons()) {
        renderColorClass(item);
      }
    }

    createAuctionMutationObserver();
  } catch (error) {
    console.error(error);
  }
}

//////// EXECUTION ////////
//// 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 {
    await Promise.race([PDAPromise, browserPromise]);
    initController();
    await loadController();
  } catch (error) {
    console.error(error);
  }
})();