BUSTR: Busting Reminder + PDA

Guess how many busts you can do without getting jailed

目前為 2023-11-25 提交的版本,檢視 最新版本

/** @format */

// ==UserScript==
// @name         BUSTR: Busting Reminder + PDA
// @namespace    http://torn.city.com.dot.com.com
// @version      1.0.1
// @description  Guess how many busts you can do without getting jailed
// @author       Adobi & Ironhydedragon
// @match        https://www.torn.com/*
// @license      MIT
// ==/UserScript==

console.log('😎 BUSTR-SCRIPT ON!!!!'); // TEST

////////  GLOBAL VARIABLES
////  State
let GLOBAL_BUSTR_STATE = {
  userSettings: {
    reminderLimits: {
      redLimit: 0,
      greenLimit: 3,
    },
    statsRefreshRate: 30,
    refetchRate: 600,
    customPenaltyThreshold: 0,
    // quickBust: true,
    // quickBail: false,
    showHardnessScore: true,
  },
  penaltyScore: 0,
  penaltyThreshold: 0,
  availableBusts: 0,
  timestampsArray: [],
  lastFetchTimestampMs: 0,
  renderedView: undefined,
};

const PDA_API_KEY = '###PDA-APIKEY###';
function isPDA() {
  const PDATestRegex = !/^(###).+(###)$/.test(PDA_API_KEY);

  return PDATestRegex;
}

////  Colors
const red = '#E54C19';
const redLight = 'rgb(255, 168, 168)';

// const orange = '#B25900';
// const orange = '#d98c00';
const orange = '#d08000';
const orangeLight = '#FFBF00';

const green = ''; // link color
const greenLight = '#85b200'; // Icon Color

const white = 'rgb(51, 51, 51)';

////  SVG Gradients
const greenSvgGradient = 'url(#sidebar_svg_gradient_regular_green_mobile';
const orangeSvgGradient = 'url(#svg_status_idle';

////  Utils Functions

function createTimestampsArray(data) {
  let timestamps = [];

  for (const entry in data.log) {
    timestamps.push(data.log[entry].timestamp);
  }

  return timestamps;
}

function calcTenHours(hours) {
  return hours / 7.2;
}

function calcPenaltyScore(timestampsArray) {
  const currentTime = Date.now() / 1000;

  let score = 0;
  let localScore = 0;
  for (const ts of timestampsArray) {
    const hours = (currentTime - ts) / 60 / 60;
    const tenHours = calcTenHours(hours);
    if (hours <= 72) {
      localScore = 128 / Math.pow(2, tenHours);
      score += localScore;
    }
  }
  return Math.floor(score);
}

function calcPenaltyThreshold(timestampsArray) {
  if (getUserSettings().customPenaltyThreshold && typeof getUserSettings().customPenaltyThreshold === 'number') return getUserSettings().customPenaltyThreshold;
  const period = 24 * 60 * 60 * 3;
  let longestSequence = 0;
  let currentSequence = 1;
  let currentMin = timestampsArray[0];
  let currentMax = timestampsArray[0];
  let firstTimestamp;

  for (let i = 1; i < timestampsArray.length; i++) {
    const TS = timestampsArray[i];
    if (currentMin - TS <= period && currentMax - TS <= period) {
      currentSequence++;
      currentMin = Math.min(currentMin, TS);
      currentMax = Math.max(currentMax, TS);
    } else {
      if (longestSequence < currentSequence) {
        firstTimestamp = currentMin;
      }
      longestSequence = Math.max(longestSequence, currentSequence);
      currentSequence = 1;
      currentMin = TS;
      currentMax = TS;
    }
  }

  let currentMaxScore = 0;
  for (let i = 0; i < timestampsArray.length - longestSequence; i++) {
    let score = 0;
    let localScore = 0;
    const initial_timestamp = timestampsArray[i];
    for (let j = 0; j < longestSequence; j++) {
      const hours = (initial_timestamp - timestampsArray[i + j]) / 60 / 60;
      const tenHours = calcTenHours(hours);
      localScore = 128 / Math.pow(2, tenHours);
      score += localScore;
    }
    currentMaxScore = Math.max(currentMaxScore, score);
  }
  return Math.floor(currentMaxScore);
}

function calcAvailableBusts(penaltyScore, penaltyThreshold) {
  return Math.floor((penaltyThreshold - penaltyScore) / 128);
}

async function fetchBustsData(apiKey) {
  try {
    const url = `https://api.torn.com/user/?selections=log&log=5360&key=${apiKey}`;

    const response = await fetch(url);
    const data = await response.json();
    setLastFecthTimestampMs();

    if (data.error) {
      if (data.error.error === 'Incorrect key' || data.error.error === 'Access level of this key is not high enough') {
        throw new Error(`Error: ${data.error.error}`);
      }
      throw new Error('Something went wrong');
    }
    return data;
  } catch (err) {
    console.error(err);
  }
}

function calcBustrStats(timestampsArray) {
  const penaltyScore = calcPenaltyScore(timestampsArray);

  const penaltyThreshold = calcPenaltyThreshold(timestampsArray);

  const availableBusts = calcAvailableBusts(penaltyScore, penaltyThreshold);

  return { penaltyScore, penaltyThreshold, availableBusts };
}

function getLevelJailDurationInfo(playerEl) {
  const levelEl = playerEl.querySelector('.level');
  const durationEl = playerEl.querySelector('.time');

  const level = +levelEl.innerText.match(/\d+/)[0];
  const hours = +durationEl.innerText.match(/\d+(?=h)/) || 0;
  const mins = +durationEl.innerText.match(/\d+(?=m)/) || 0;
  const durationInHours = hours + mins / 60;

  return [level, +durationInHours];
}

function calcHardnessScore(level, durationInHours) {
  return Math.floor(level * (durationInHours + 3));
}

function renderHardnessScore(playerEl, hardnessScore) {
  playerEl.querySelector('.bustr-hardness-score').textContent = hardnessScore;
}

function sortByHardnessScore(playerEl, hardnessScore) {
  playerEl.style.order = hardnessScore;
}

//// Callback functions
function submitFormCallback() {
  const inputEl = document.querySelector('#bustr-form__input');
  const submitBtnEl = document.querySelector('#bustr-form__submit');

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

function inputValidatorCallback(event) {
  const inputEl = document.querySelector('#bustr-form__input');
  const submitBtnEl = document.querySelector('#bustr-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;
  }
}

async function successfulBustMutationCallback(mutationList, observer) {
  try {
    for (const mutation of mutationList) {
      if (!mutation.target.innerText) return;
      if (mutation.target.innerText.match(/^(You busted ).+/) && mutation.removedNodes.length > 0) {
        console.log('👀 You busted...', mutation); // TEST
        observer.disconnect();
        addOneTimestampsArray(Math.floor(Date.now() / 1000));
        await loadController();
        successfulBustUpdateController();
      }
    }
  } catch (err) {
    console.error(err);
  }
}

function mountJailPlayerCallback(mutationList, observer) {
  for (const mutation of mutationList) {
    if (mutation.target.classList.contains('user-info-list-wrap') && mutation.addedNodes.length > 1) {
      hardnessScoreController();
      observer.disconnect();
    }
  }
}

//// Observers
function createJailMutationObserver() {
  const jailObserver = new MutationObserver(successfulBustMutationCallback);
  jailObserver.observe(document, {
    attributes: false,
    childList: true,
    subtree: true,
  });
}

function createMountJailPlayerOberserver() {
  const mountJailPlayerObserver = new MutationObserver(mountJailPlayerCallback);
  mountJailPlayerObserver.observe(document, {
    attributes: false,
    childList: true,
    subtree: true,
  });
}

////////  MODEL ////////
////  Getters and Setters
function setGlobalBustrState(newState) {
  GLOBAL_BUSTR_STATE = { ...GLOBAL_BUSTR_STATE, ...newState };
  localStorage.setItem('globalBustrState', JSON.stringify(GLOBAL_BUSTR_STATE));
  saveGlobalBustrState();
}
function getGlobalBustrState() {
  return GLOBAL_BUSTR_STATE;
}
function loadGlobalBustrState() {
  if (!localStorage.getItem('globalBustrState')) return;
  const loadedState = JSON.parse(localStorage.getItem('globalBustrState'));
  GLOBAL_BUSTR_STATE = { ...GLOBAL_BUSTR_STATE, ...loadedState };
  return localStorage.getItem('globalBustrState');
}
function saveGlobalBustrState() {
  localStorage.setItem('globalBustrState', JSON.stringify(getGlobalBustrState()));
}
function deleteGlobalBustrState() {
  GLOBAL_BUSTR_STATE = {
    userSettings: {
      reminderLimits: {
        redLimit: 0,
        greenLimit: 3,
      },
    },
    penaltyScore: 0,
    penaltyThreshold: 0,
    availableBusts: 0,
    timestampsArray: [],
  };
  localStorage.removeItem('bustrGlobalState');
}

function getMyViewportWidthType() {
  let width = visualViewport.width;

  if (width > 1000) return 'Desktop';
  if (width < 1000 || width) return 'Mobile';
  throw new Error('Visual viewport not loaded');
}

function setApiKey(apiKey) {
  localStorage.setItem('bustrApiKey', JSON.stringify(apiKey));
}
function getApiKey() {
  if (isPDA()) return PDA_API_KEY;

  if (!localStorage.getItem('bustrApiKey')) return;

  return JSON.parse(localStorage.getItem('bustrApiKey'));
}

function deleteApiKey() {
  localStorage.removeItem('bustrApiKey');
}

function setUserSettings(newUserSettings, currentState) {
  currentState = currentState || getGlobalBustrState();

  const newState = { ...currentState, userSettings: newUserSettings };
  setGlobalBustrState(newState);
}
function getUserSettings() {
  return getGlobalBustrState().userSettings;
}

function setRenderedView(newRenderedView, currentState) {
  currentState = currentState || getGlobalBustrState();

  const newState = { ...currentState, renderedView: newRenderedView };
  setGlobalBustrState(newState);
}
function getRenderedView(newRenderedView, currentState) {
  return getGlobalBustrState().renderedView;
}

function setTimestampsArray(newTimestampsArr, currentState) {
  currentState = currentState || getGlobalBustrState();

  return setGlobalBustrState({
    ...currentState,
    timestampsArray: newTimestampsArr,
  });
}
function addOneTimestampsArray(timestamp, currentState) {
  currentState = currentState || getGlobalBustrState();

  const newTimestampsArr = [timestamp, ...currentState.timestampsArray];
  setTimestampsArray(newTimestampsArr);
}
function getTimestampsArray() {
  return getGlobalBustrState().timestampsArray;
}

function setLastFecthTimestampMs(currentState) {
  currentState = currentState || getGlobalBustrState();

  const currentTimestampMs = Date.now();
  setGlobalBustrState({
    ...currentState,
    lastFetchTimestampMs: currentTimestampMs,
  });
}
function getLastFecthTimestampMs() {
  return getGlobalBustrState().lastFetchTimestampMs;
}

function setPenaltyThreshold(newPenaltyThreshold, currentState) {
  currentState = currentState || getGlobalBustrState();

  const newState = { ...currentState, penaltyThreshold: newPenaltyThreshold };
  setGlobalBustrState(newState);
}
function getPenaltyThreshold() {
  return getGlobalBustrState().penaltyThreshold;
}

function setPenaltyScore(newPenaltyScore, currentState) {
  currentState = currentState || getGlobalBustrState();

  const newState = { ...currentState, penaltyScore: newPenaltyScore };
  setGlobalBustrState(newState);
}
function getPenaltyScore() {
  return getGlobalBustrState().penaltyScore;
}

function setAvailableBusts(newAvailableBusts, currentState) {
  currentState = currentState || getGlobalBustrState();

  const newState = { ...currentState, availableBusts: newAvailableBusts };
  setGlobalBustrState(newState);
}
function getAvailableBusts() {
  return getGlobalBustrState().availableBusts;
}

////////  VIEW  ////////
////  Stylesheet
const bustrStylesheetHTML = `<style>
  .bustr--green {
    --color: ${green}
  }
  .bustr--orange {
    --color: ${orange}
  }
  .bustr--red {
    --color: ${red}
  }
  .dark-mode.bustr--green,
  .bustr--green .swiper-slide {
    --color: ${greenLight}
  }
  .dark-mode.bustr--orange,
  .bustr--orange .swiper-slide {
    --color: ${orangeLight}
  }
  .dark-mode.bustr--red,
  .bustr--red .swiper-slide {
    --color: ${redLight}
  }
  .bg-gradient--green{
    --gradient-green: linear-gradient(to bottom, rgba(143, 113, 113, 1) 0, rgba(92, 62, 62, 1) 100%);
  }

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

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

  #bustr-form.header-wrapper-top input {
    background: linear-gradient(0deg,#111,#000);
    border-radius: 5px;
    box-shadow: 0 1px 0 hsla(0,0%,100%,.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;
  }
  #bustr-form.header-wrapper-top a {
    margin: 0 8px;
  }

  #nav-jail .bustr-stats,
  #bustr-context .bustr-stats {
    color: var(--color, inherit);
  }
  #nav-jail .bustr-stats span {
    margin-left: unset;
  }

  #bustr-context.contextMenu___bjhoL {
    display: none;
    left: unset;
    right: -92px;
    padding: 0 8px;
  }
  .contextMenuActive___e6i_B #bustr-context.contextMenu___bjhoL {
    display: flex;
  }
  #bustr-context.contextMenu___bjhoL .arrow___tKP13 {
    right: unset;
    left: -6px;
    border-width: 8px 6px 8px 0;
    border-color: transparent #444 transparent transparent;
  }
  #bustr-context.contextMenu___bjhoL .arrow___tKP13:before {
    border-color: transparent #373636 transparent transparent;
    border-width: 6px 5px 6px 0;
    content: "";
    left: unset;
    right: -6px;
    top: -6px;
  }

  #prefs-tab-menu #bustr-settings {
    display: none;
  }
  #prefs-tab-menu #bustr-settings.active {
    display: block;
  }
  #bustr-settings input[type="number"] {
    height: 24px;
    width: 48px;
    padding: 1px 5px;
    text-align: center;
  }

  #bustr-settings-dropdown:hover {
    background: #fff;
  }
  .dark-mode #prefs-tab-menu #bustr-settings-dropdown:hover {
    background: #444;
  }
  #prefs-tab-menu #bustr-settings-sidetab.active {
    background: #fff;
    color: #999
  }
  .dark-mode #prefs-tab-menu #bustr-settings-sidetab.active {
    background: #444;
    color: #999
  }

  #body .users-list-title {
    display: flex;
    justify-content: start;
    align-items: center;
  }
  #body .users-list-title .title{
    width: 269px;
  }
  #body .users-list-title .time{
    width: 50px;
  }
  #body .users-list-title .level{
    width: 53px;
  }
  #body .users-list-title .reason{
    width: 205px;
  }
  #body .users-list-title .hardness{
    display: block;
    width: 79px;
    text-align: center;
  }

  #body .user-info-list-wrap > li .info-wrap .hardness {
    display: block; 
    text-align: center;
  }
  #body .user-info-list-wrap > li .info-wrap .hardness span.title {
    display: none;
  }

  #body .user-info-list-wrap {
    display: flex;
    flex-direction: column;
    justify-content: start;
    align-items: center;
  }
  #body .user-info-list-wrap > li  {
    display: flex; 
    flex-wrap: wrap; 
    justify-content: start; 
    align-items: center;
  }

  #body .user-info-list-wrap > li .info-wrap {
    display: flex; 
    flex-wrap: wrap;
    justify-content: start; 
    align-items: center;
  }
  #body .user-info-list-wrap > li .info-wrap .time {
    width: 54px;
  }
  #body .user-info-list-wrap > li .info-wrap .level {
    width: 57px;
  }
  #body .user-info-list-wrap > li .info-wrap .reason {
    width: 193px;
  }
  #body .user-info-list-wrap > li .info-wrap .hardness {
    width: 50px;
  }

  @media screen and (max-width:1000px) {
    #bustr-form.header-wrapper-top h2 {
      width: 148px;
    }
    #bustr-form.header-wrapper-top input {
      margin-left: 10px;
    }
  }
  @media screen and (max-width:784px) {
    #bustr-form.header-wrapper-top h2 {
      font-size: 16px;
      width: 80px;
    }
    #body .users-list-title .hardness{
      display: none;
    }
    #body .user-info-list-wrap > li .info-wrap .hardness span.title{
      display: block;
    }
    #body .user-info-list-wrap > li .info-wrap .reason {
      width: 164px;
      border-right: 1px solid rgb(34, 34, 34);
    }
    #body .user-info-list-wrap > li .info-wrap .hardness {
      width: 64px;
    }
  }
    @media screen and (max-width:386px) {
      
      #body .user-info-list-wrap > li .info-wrap .time {
        width: 98px;
        height: 37px;
      }
      #body .user-info-list-wrap > li .info-wrap .level {
        width: 91px;
        height: 37px;
      }
      #body .user-info-list-wrap > li .info-wrap .reason {
        width: 171px;
        height: 24px;
        border-right: 1px solid rgb(34, 34, 34);
      }
      #body .user-info-list-wrap > li .info-wrap .hardness {
        width: 107px;
      }
    }
  }
    </style>`;
function renderBustrStylesheet() {
  const headEl = document.querySelector('head');
  headEl.insertAdjacentHTML('beforeend', bustrStylesheetHTML);
}

function renderBustrColorClass(availableBusts) {
  const navJailEl = document.querySelector('#nav-jail');

  const redLimit = typeof getUserSettings().reminderLimits.redLimit === 'number' ? getUserSettings().reminderLimits.redLimit : 0;
  const greenLimit = typeof getUserSettings().reminderLimits.greenLimit === 'number' ? getUserSettings().reminderLimits.greenLimit : 3;

  if (+availableBusts <= redLimit) {
    document.body.classList.add('bustr--red');
    return;
  }

  if (+availableBusts >= greenLimit) {
    document.body.classList.add('available___ZS04X', 'bustr--green');
    return;
  }

  if (availableBusts > redLimit && availableBusts < greenLimit) {
    document.body.classList.add('bustr--orange');
  }
}
//// Init form view
function renderBustrForm() {
  const topHeaderBannerEl = document.querySelector('#topHeaderBanner');
  const bustrFormHTML = `
      <div id="bustr-form" class="header-wrapper-top">
        <div class="container clear-fix">
          <h2>Bustr API</h2>
          <input
            id="bustr-form__input"
            type="text"
            placeholder="Enter a full-acces API key..."
          />
          <a href="#" id="bustr-form__submit"  type="btn" disabled><span class="link-text">Submit</span</button>
        </div>
      </div>`;

  topHeaderBannerEl.insertAdjacentHTML('afterbegin', bustrFormHTML);
}

function dismountBustrForm() {
  document.querySelector('#bustr-form').remove();
}

function renderBustrSettingsTabs() {
  const sideMenuTabsElArr = [...document.querySelectorAll('#prefs-tab-menu .headers li')];
  const dropdownCategoriesBtn = document.querySelector('#categories-button');
  const dropdownMenuListEl = document.querySelector('ul.ui-selectmenu-menu-dropdown');
  const prefsTitle = document.querySelector('.prefs-tab-title');
  const prefsContentArr = [...document.querySelectorAll('.prefs-cont')];

  const bustrSettingsSideTabHTML = `
    <li class="delimiter"></li>
    <li id="bustr-settings-sidetab" class="c-pointer ui-state-default ui-corner-top ui-tabs-active" data-title-name="Change your general settings" role="tab" tabindex="0" aria-controls="settings" aria-labelledby="ui-id-2" aria-selected="true">
      <a class="t-gray-6 bold h settings ui-tabs-anchor" href="#settings" role="presentation" tabindex="-1" id="bustr-settings-sidetab__link" i-data="i_192_87_149_34">Bustr settings</a>
    </li>
    `;

  const bustrSettingsDropdownTabHTML = `
  <li role="presentation" id='bustr-settings-dropdown'>
    <a href="#nogo" tabindex="-1" role="option" aria-selected="false" id="bustr-dropdown__select">Bustr settings</a>
  </li>`;

  sideMenuTabsElArr[6].insertAdjacentHTML('afterend', bustrSettingsSideTabHTML);

  dropdownMenuListEl.insertAdjacentHTML('afterbegin', bustrSettingsDropdownTabHTML);
  dropdownCategoriesBtn.addEventListener('click', () => {
    const busterDropdownIsChild = [...dropdownMenuListEl.children].filter((child) => child.id === 'bustr-settings-dropdown');
    if (!busterDropdownIsChild) {
      dropdownMenuListEl.insertAdjacentHTML('afterbegin', bustrSettingsDropdownTabHTML);
    }
  });

  const dropdownMenuItemsArr = [...dropdownMenuListEl.querySelectorAll('li')];
  // listeners for dropdown li click
  dropdownMenuItemsArr.forEach((li, i, arr) => {
    li.addEventListener('click', (e) => {
      if (li.id === 'bustr-settings-dropdown') {
        prefsContentArr.forEach((el) => {
          if (el.id !== 'bustr-settings') el.style.display = 'none';
        });
        document.querySelector('#bustr-settings').style.display = 'block';
        arr.forEach((li) => li.classList.remove(''));
        dropdownCategoriesBtn.textContent = 'Bustr settings';
        prefsTitle.textContent = 'Change your bust reminder settings';
      }
      if (li.id !== 'bustr-settings-dropdown') {
        document.querySelector('#bustr-settings').classList.remove('active');
        document.querySelector('#bustr-settings').style.display = 'none';
      }
    });
    sideMenuTabsElArr.forEach((li, i, arr) => {
      li.addEventListener('click', (e) => {
        if (li.id === 'bustr-settings-sidetab') {
          prefsContentArr.forEach((el) => {
            if (el.id !== 'bustr-settings') el.style.display = 'none';
          });
          document.querySelector('#bustr-settings').style.display = 'block';
          arr.forEach((li) => li.classList.remove(''));
          dropdownCategoriesBtn.textContent = 'Bustr settings';
          prefsTitle.textContent = 'Change your bust reminder settings';
        }
        if (li.id !== 'bustr-settings-sidetab') {
          document.querySelector('#bustr-settings').classList.remove('active');
          document.querySelector('#bustr-settings').style.display = 'none';
        }
      });
    });
  });
  // if, #bustr-settings-dropdown
  // then, set other forms to display none, add active class to bustr form, set categores button innerText to bustr settings
  // if, not #bustr-settings-dropdown
  // then, remove active class from form
}

function renderBustrSettingsForm() {
  const prefsTabTitleEl = document.querySelector('.prefs-tab-title');
  prefsTabTitleEl.textContent = 'Change your bust reminder settings';

  const sideMenuTabsListEl = document.querySelector('#prefs-tab-menu .headers');
  // show tabs class : ui-tabs-active
  // active tab class : ui-state-active

  bustrSettingsFormHTML = `
    <div id="bustr-settings" class="prefs-cont left ui-tabs-panel ui-widget-content ui-corner-bottom" aria-labelledby="ui-id-3" role="tabpanel" aria-expanded="true" aria-hidden="false">
        <div class="inner-block b-border-c t-border-f">
          <ul class="prefs-list small-select-menu-wrap">
            <li>
              <p class="m-bottom5">Custom Penalty Threshold:</p>
              <input type="number" name="customThreshold" id="bustr-custom-threshold" size="5" value="0" min="0" />
            </li>
          </ul>
        </div>
        <div class="inner-block b-border-c t-border-f">
          <ul class="prefs-list small-select-menu-wrap">
            <li>
              <p class="m-bottom5">Custom Penalty Threshold:</p>
              <input type="number" name="customThreshold" id="bustr-custom-threshold" size="5" value="0" min="0" />
            </li>
          </ul>
        </div>
        <div class="inner-block b-border-c t-border-f">
          <ul class="prefs-list attack-pref-block small-select-menu-wrap">
            <li role="radiogroup">
              <div class="title left">Quick Bust</div>
              <div class="choice-container left-position">
                <input id="quick-bust-on" class="radio-css" type="radio" name="quick-bust" value="true" checked="checked" />
                <label for="quick-bust-on" class="marker-css">On</label>
              </div>
              <div class="choice-container right-position">
                <input id="quick-bust-off" class="radio-css" type="radio" name="quick-bust" value="false" />
                <label for="quick-bust-off" class="marker-css">Off</label>
              </div>
              <div class="clear"></div>
            </li>
            <li role="radiogroup">
              <div class="title left">Quick Bail</div>
              <div class="choice-container left-position">
                <input id="quick-bail-on" class="radio-css" type="radio" name="quick-bail" value="true" checked="checked" />
                <label for="quick-bail-on" class="marker-css">On</label>
              </div>
              <div class="choice-container right-position">
                <input id="quick-bail-off" class="radio-css" type="radio" name="quick-bail" value="false" />
                <label for="quick-bail-off" class="marker-css">Off</label>
              </div>
              <div class="clear"></div>
            </li>
          </ul>
        </div>
        <div class="inner-block b-border-c t-border-f">
          <ul class="prefs-list small-select-menu-wrap">
            <li>
              <p class="m-bottom5">Red Limit:</p>
              <input type="number" name="redLimit" id="bustr-red-input" size="5" value="0" min="0" />
            </li>
            <li>
              <p class="m-top10 m-bottom5">Green Limit:</p>
              <input id="bustr-green-input" type="number" name="Red Limit" value="3" size="5" />
            </li>
          </ul>
        </div>
        <div class="inner-block b-border-c t-border-f">
          <ul class="prefs-list small-select-menu-wrap">
            <li>
              <p class="m-top10 m-bottom5">Stats Refresh Rate (seconds):</p>
              <input type="number" name="city" id="bustr-refresh-input" size="5" value="30" />
            </li>
            <li>
              <p class="m-top10 m-bottom5">API Refresh Rate (minutes):</p>
              <input type="number" name="API Refetch Rate" id="bustr-refetch-input" value="10" />
            </li>
          </ul>
        </div>
        <div class="inner-block t-border-f">
          <div class="btn-wrap silver">
            <div class="btn">
              <input class="torn-btn update" type="submit" value="SAVE" id="bustr-save-btn" />
            </div>
          </div>
        </div>
      </div>`;

  sideMenuTabsListEl.insertAdjacentHTML('afterend', bustrSettingsFormHTML);
}

// function renderJailIcon() {
//   console.log('REPLACE JAIL ICON'); // TEST
//   const fill = 'url(#sidebar_svg_gradient_regular_green_mobile';

//   const jailIconEl = document.querySelector('#nav-jail svg');
//   const jailIconContainerEl =
//     document.querySelector('#nav-jail svg').parentElement;

//   const greenJailHTML = `
//       <svg xmlns="http://www.w3.org/2000/svg" class="default___XXAGt " filter fill="${fill}" stroke="transparent" stroke-width="0" width="17" height="17" viewBox="0 1 17 17">
//         <path d="M11.56,1V18h2V1Zm-5,12.56h4v-2h-4ZM0,13.56H2.56v-2H0Zm14.56,0h2.5v-2h-2.5Zm-8-6h4v-2h-4ZM0,7.56H2.56v-2H0Zm14.56,0h2.5v-2h-2.5ZM3.56,1V18h2V1Z" filter="url(#svg_sidebar_mobile)"></path>
//         <path d="M11.56,1V18h2V1Zm-5,12.56h4v-2h-4ZM0,13.56H2.56v-2H0Zm14.56,0h2.5v-2h-2.5Zm-8-6h4v-2h-4ZM0,7.56H2.56v-2H0Zm14.56,0h2.5v-2h-2.5ZM3.56,1V18h2V1Z"></path>
//       </svg>`;

//   jailIconEl.remove();
//   jailIconContainerEl.insertAdjacentHTML('afterbegin', greenJailHTML);
// }

function renderBustrStats(statsObj) {
  for (const [key, value] of Object.entries(statsObj)) {
    const statsElArr = [...document.querySelectorAll(`.bustr-stats__${key}`)];
    statsElArr.forEach((el) => (el.textContent = value));
  }
}

//// Desktop view
function renderBustrDesktopView() {
  const jailLinkEl = document.querySelector('#nav-jail a');
  const statsHTML = `
      <span class="amount___p8QZX bustr-stats">
        <span class="bustr-stats__penaltyScore">#</span> / <span class="bustr-stats__penaltyThreshold">#</span> : <span class="bustr-stats__availableBusts">#</span>
      </span>`;

  jailLinkEl.insertAdjacentHTML('beforeend', statsHTML);
}

//// Mobile view
function renderMobileBustrNotification() {
  const navJailLinkEl = document.querySelector('#nav-jail a');
  const notificationHTML = `
  <div class="mobileAmount___ua3ye bustr-stats"><span class="bustr-stats__availableBusts">#</span></div>`;

  navJailLinkEl.insertAdjacentHTML('beforebegin', notificationHTML);
}

function renderBustrMobileView() {
  renderMobileBustrNotification();

  const bustrContextMenuHTML = `
      <div id="bustr-context" class='contextMenu___bjhoL bustr-context-menu'>
        <span class='linkName___FoKha bustr-stats'>
        <span class="bustr-stats__penaltyScore">#</span> / <span class="bustr-stats__penaltyThreshold">#</span> : <span class="bustr-stats__availableBusts">#</span>
        </span>
        <span class='arrow___tKP13 bustr-arrow'></span>
      </div>`;

  const navJailEl = document.querySelector('#nav-jail');
  navJailEl.insertAdjacentHTML('afterend', bustrContextMenuHTML);
}

function renderHardnessJailView() {
  const headingsContainerEl = document.querySelector('.users-list-title');
  const hardnessTitleHTML = `
    <span class="hardness title-divider divider-spiky">Hardness</span>`;
  if (!headingsContainerEl.querySelector('span.hardness')) {
    headingsContainerEl.children[3].insertAdjacentHTML('afterend', hardnessTitleHTML);
  }

  const playerRowsArr = [...document.querySelectorAll('.user-info-list-wrap > li')];
  playerRowsArr.forEach((el) => {
    const playerInfoContainerEl = el.querySelector('.info-wrap');
    const hardnessScoreHTML = `
      <span class="hardness reason">
        <span class="title bold">HARDNESS</span>
        <span class="bustr-hardness-score">#####</span>
      </span>`;
    playerInfoContainerEl.children[2].insertAdjacentHTML('afterend', hardnessScoreHTML);
  });
}

////////  CONTROLLERS  ////////
async function initController() {
  try {
    // render stylesheet
    renderBustrStylesheet();

    // check if apiKey is saved
    // if saved exit function
    if (isPDA() && !getApiKey()) {
      setApiKey(PDA_API_KEY);
    }

    if (!visualViewport.width) {
      await new Promise((res) => {
        document.addEventListener('load', () => res());
      });
    }

    if (getMyViewportWidthType() === 'Desktop') {
      renderBustrDesktopView();
      setRenderedView('Desktop');
    }
    if (getMyViewportWidthType() === 'Mobile') {
      renderBustrMobileView();
      setRenderedView('Mobile');
    }

    if (getApiKey()) return;

    // if not saved render bustr form
    renderBustrForm();

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

//// load
async function loadController() {
  try {
    // guard clause if no api key
    if (!getApiKey()) return;

    if (loadGlobalBustrState()) {
      loadGlobalBustrState();
    }

    // fetch data
    const refetchRate = typeof getUserSettings().refetchRate === 'number' && getUserSettings().refetchRate > 0 ? getUserSettings().refetchRate : 600;
    if (getTimestampsArray().length === 0 || Date.now() - getLastFecthTimestampMs() > 1000 * refetchRate || !getLastFecthTimestampMs()) {
      const data = await fetchBustsData(getApiKey());
      setTimestampsArray(createTimestampsArray(data));
    }

    const statsObj = calcBustrStats(getTimestampsArray());
    setPenaltyScore(statsObj.penaltyScore);
    setPenaltyThreshold(statsObj.penaltyThreshold);
    setAvailableBusts(statsObj.availableBusts);

    // render color class
    renderBustrColorClass(getAvailableBusts());

    // render stats
    renderBustrStats(statsObj);
  } catch (err) {
    // deleteApiKey();
    console.error(err);
  }
}

function successfulBustUpdateController() {
  // update after a successful bust
  const origin = window.location.origin;
  const pathname = window.location.pathname;

  createJailMutationObserver();
}

function refreshStatsController() {
  const statsRefreshRate = typeof getUserSettings().statsRefreshRate === 'number' && getUserSettings().statsRefreshRate > 0 ? getUserSettings().statsRefreshRate : 30;
  setInterval(async () => {
    await loadController();
  }, statsRefreshRate * 1000 || 30000);
}

function viewportResizeController() {
  visualViewport.addEventListener('resize', async (e) => {
    if (!getRenderedView()) return;

    const viewportWidthType = getMyViewportWidthType();
    if (viewportWidthType !== getRenderedView()) {
      initController();
      await loadController();
    }
  });
}

function userSettingsController() {
  if (window.location.pathname !== '/preferences.php') return;
  renderBustrSettingsTabs();
  renderBustrSettingsForm();
}

function hardnessScoreController() {
  if (window.location.pathname !== '/jailview.php') return;
  createMountJailPlayerOberserver();

  renderHardnessJailView();
  const playersArr = [...document.querySelectorAll('ul.user-info-list-wrap > li')];

  for (const playerEl of playersArr) {
    const [level, durationInHours] = getLevelJailDurationInfo(playerEl);
    const hardnessScore = calcHardnessScore(level, durationInHours);
    renderHardnessScore(playerEl, hardnessScore);
    sortByHardnessScore(playerEl, hardnessScore);
  }
}

//// 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 function () {
  try {
    await Promise.race([PDAPromise, browserPromise]);
    initController();
    await loadController();
    // userSettingsController();
    if (getUserSettings().showHardnessScore) {
      hardnessScoreController();
    }
    successfulBustUpdateController();
    refreshStatsController();
    viewportResizeController();
  } catch (err) {
    console.error(err);
  }
})();

QingJ © 2025

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