Auto gym switch

Automatically switches your gym before training

// ==UserScript==
// @name         Auto gym switch
// @namespace    https://gitgud.com/stephenlynx
// @version      1.2.4
// @description  Automatically switches your gym before training
// @author       Stephen Lynx
// @license      MIT
// @match        https://www.torn.com/gym.php
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @run-at       document-start
// @grant        none
// ==/UserScript==
var lynx = {};

lynx.mainRatioOptions = ['No main gym', 'Str', 'Def', 'Spe', 'Dex'];
lynx.secondaryRatioOptions = ['No secondary gym', 'Def/Dex', 'Str/Spe'];

lynx.red = '#d83500';
lynx.green = '#00a500';

lynx.gymInfo = {
  1 : {
    'str': 2,
    'spe': 2,
    'def': 2,
    'dex': 2
  },
  2 : {
    'str': 2.4,
    'spe': 2.4,
    'def': 2.7,
    'dex': 2.4
  },
  3 : {
    'str': 2.7,
    'spe': 3.2,
    'def': 3.0,
    'dex': 2.7
  },
  4 : {
    'str': 3.2,
    'spe': 3.2,
    'def': 3.2,
    'dex': 0
  },
  5 : {
    'str': 3.4,
    'spe': 3.6,
    'def': 3.4,
    'dex': 3.2
  },
  6 : {
    'str': 3.4,
    'spe': 3.6,
    'def': 3.6,
    'dex': 3.8
  },
  7 : {
    'str': 3.7,
    'spe': 0,
    'def': 3.7,
    'dex': 3.7
  },
  8 : {
    'str': 4,
    'spe': 4,
    'def': 4,
    'dex': 4
  },
  9 : {
    'str': 4.8,
    'spe': 4.4,
    'def': 4,
    'dex': 4.2
  },
  10 : {
    'str': 4.4,
    'spe': 4.6,
    'def': 4.8,
    'dex': 4.4
  },
  11 : {
    'str': 5,
    'spe': 4.6,
    'def': 5.2,
    'dex': 4.6
  },
  12 : {
    'str': 5,
    'spe': 5.2,
    'def': 5,
    'dex': 5
  },
  13 : {
    'str': 5,
    'spe': 5.4,
    'def': 4.8,
    'dex': 5.2
  },
  14 : {
    'str': 5.5,
    'spe': 5.7,
    'def': 5.5,
    'dex': 5.2
  },
  15 : {
    'str': 0,
    'spe': 5.5,
    'def': 5.5,
    'dex': 5.7
  },
  16 : {
    'str': 6,
    'spe': 6,
    'def': 6,
    'dex': 6
  },
  17 : {
    'str': 6,
    'spe': 6.2,
    'def': 6.4,
    'dex': 6.2
  },
  18 : {
    'str': 6.5,
    'spe': 6.4,
    'def': 6.2,
    'dex': 6.2
  },
  19 : {
    'str': 6.4,
    'spe': 6.5,
    'def': 6.4,
    'dex': 6.8
  },
  20 : {
    'str': 6.4,
    'spe': 6.4,
    'def': 6.8,
    'dex': 7
  },
  21 : {
    'str': 7,
    'spe': 6.4,
    'def': 6.4,
    'dex': 6.5
  },
  22 : {
    'str': 6.8,
    'spe': 6.5,
    'def': 7,
    'dex': 6.5
  },
  23 : {
    'str': 6.8,
    'spe': 7,
    'def': 7,
    'dex': 6.8
  },
  24 : {
    'str': 7.3,
    'spe': 7.3,
    'def': 7.3,
    'dex': 7.3
  },
  25 : {
    'str': 0,
    'spe': 0,
    'def': 7.5,
    'dex': 7.5
  },
  26 : {
    'str': 7.5,
    'spe': 7.5,
    'def': 0,
    'dex': 0
  },
  27 : {
    'str': 8,
    'spe': 0,
    'def': 0,
    'dex': 0
  },
  28 : {
    'str': 0,
    'spe': 0,
    'def': 8,
    'dex': 0
  },
  29 : {
    'str': 0,
    'spe': 8,
    'def': 0,
    'dex': 0
  },
  30 : {
    'str': 0,
    'spe': 0,
    'def': 0,
    'dex': 8
  },
  31 : {
    'str': 9,
    'spe': 9,
    'def': 9,
    'dex': 9
  },
  32 : {
    'str': 10,
    'spe': 10,
    'def': 10,
    'dex': 10
  },
  33 : {
    'str': 3.4,
    'spe': 3.4,
    'def': 4.6,
    'dex': 0
  }
};

// lynx.currentGym is used to store the current gym being used
lynx.classList = ['specialist', 'heavyweight', 'middleweight', 'lightweight', 'jail'];

lynx.currentStats = {};

lynx.abbreviations = ['k', 'm', 'b', 't'];

lynx.picks = {
  'str': [],
  'def': [],
  'spe': [],
  'dex': []
};

lynx.ratioLabels = {};

lynx.stats = ['str', 'def', 'spe', 'dex'];

lynx.checkGym = function(gymId) {

  lynx.gymInfo[gymId].checked = true;
  var gymStats = lynx.gymInfo[gymId];

  for (var stat of lynx.stats) {

    if (gymStats[stat]) {
      lynx.picks[stat].push({
        id: gymId,
        gain: gymStats[stat]
      });
    }

  }

};

lynx.sortGyms = function(a, b) {

  if (a.gain === b.gain) {
    return b.id - a.id;
  } else {
    return b.gain - a.gain;
  }

};

lynx.pickGyms = function(gymStatus) {

  for (var gymClass of lynx.classList) {

    for (var gym of gymStatus[gymClass]) {

      lynx.gymInfo[gym.id].name = gym.name;
      lynx.gymInfo[gym.id].cost = gym.energyCost;

      if (! (gym.status === 'available' || gym.status === 'active')) {
        continue;
      }

      if (gym.status === 'active') {
        lynx.currentGym = gym.id;
      }

      lynx.checkGym(gym.id);

    }
  }

  for (var stat in lynx.picks) {
    lynx.picks[stat].sort(lynx.sortGyms);
  }

};

lynx.formatNumber = function(number) {

  var abbreviationIndex = -1;

  while (number > 1000 && abbreviationIndex < lynx.abbreviations.length) {
    number /= 1000;
    abbreviationIndex++;
  }

  return Math.trunc(number) + (abbreviationIndex >= 0 ? lynx.abbreviations[abbreviationIndex] : '');

};

lynx.calculateRatios = function() {

  for (var labelStat in lynx.ratioLabels) {
    lynx.ratioLabels[labelStat].innerHTML = '';
  }

  var mainGym = lynx.mainGymSelect.selectedIndex;
  localStorage.setItem('lynxMainGym', mainGym);
  var secondaryGym = lynx.secondaryGymSelect.selectedIndex;
  localStorage.setItem('lynxSecondaryGym', secondaryGym);

  if (mainGym) {
    var mainStatName = lynx.stats[mainGym - 1];
    var secondHighestStatName;
    var secondHighestStatValue = 0;

    for (var stat of lynx.stats) {

      if (stat !== mainStatName && lynx.currentStats[stat] > secondHighestStatValue) {
        secondHighestStatName = stat;
        secondHighestStatValue = lynx.currentStats[stat];
      }

    }

    var mainTarget = secondHighestStatValue * 1.25;

    if (mainTarget > lynx.currentStats[mainStatName]) {

      lynx.ratioLabels[mainStatName].innerHTML = 'Must gain ' + lynx.formatNumber(mainTarget - lynx.currentStats[mainStatName]);
      lynx.ratioLabels[mainStatName].style.color = lynx.red;

    } else {

      var lead = lynx.currentStats[mainStatName] / 1.25;
      lead -= secondHighestStatValue;

      lynx.ratioLabels[secondHighestStatName].innerHTML = 'Can gain ' + lynx.formatNumber(lead);
      lynx.ratioLabels[secondHighestStatName].style.color = lynx.green;
    }

  }

  if (secondaryGym) {

    var defStats = ['dex', 'def'];
    var offStats = ['str', 'spe'];

    var defSum = lynx.currentStats.def + lynx.currentStats.dex;

    var offSum = lynx.currentStats.str + lynx.currentStats.spe;

    var secondaryTarget = (secondaryGym === 1 ? offSum: defSum) * 1.25;

    var secondaryCurrent = secondaryGym === 1 ? defSum: offSum;

    if (secondaryTarget > secondaryCurrent) {

      for (let stat of secondaryGym === 1 ? defStats: offStats) {
        lynx.ratioLabels[stat].innerHTML = 'Must gain ' + lynx.formatNumber(secondaryTarget - secondaryCurrent);
        lynx.ratioLabels[stat].style.color = lynx.red;
      }

    } else {
      var secondaryLead = secondaryCurrent / 1.25;
      secondaryLead -= secondaryGym === 1 ? offSum: defSum;

      for (let stat of secondaryGym === 1 ? offStats: defStats) {
        lynx.ratioLabels[stat].innerHTML = 'Can gain ' + lynx.formatNumber(secondaryLead);
        lynx.ratioLabels[stat].style.color = lynx.green;
      }

    }

  }
};

lynx.setupRatios = function(stats) {

  lynx.ratioSetup = true;

  for (var stat in stats) {
    lynx.currentStats[stat.substring(0, 3)] = +stats[stat].value.replace(/,/g, '');
  }

  var parent = document.getElementsByClassName('content-wrapper')[0];

  var ratioDiv = document.createElement('div');
  ratioDiv.className = 'lynx';

  lynx.mainGymSelect = document.createElement('select');
  lynx.mainGymSelect.id = 'lynx_mainRatio';

  lynx.secondaryGymSelect = document.createElement('select');
  lynx.secondaryGymSelect.id = 'lynx_secondaryRatio';

  ratioDiv.appendChild(lynx.mainGymSelect);

  for (var mainOption of lynx.mainRatioOptions) {

    var option = document.createElement('option');
    option.innerHTML = mainOption;
    lynx.mainGymSelect.appendChild(option);
  }

  lynx.mainGymSelect.selectedIndex = +localStorage.getItem('lynxMainGym') || 0;
  lynx.mainGymSelect.onchange = lynx.calculateRatios;

  ratioDiv.appendChild(lynx.secondaryGymSelect);

  for (var secondaryOptionText of lynx.secondaryRatioOptions) {
    var secondaryOption = document.createElement('option');
    secondaryOption.innerHTML = secondaryOptionText;
    lynx.secondaryGymSelect.appendChild(secondaryOption);
  }

  lynx.secondaryGymSelect.selectedIndex = +localStorage.getItem('lynxSecondaryGym') || 0;
  lynx.secondaryGymSelect.onchange = lynx.calculateRatios;

  parent.appendChild(ratioDiv);

};

lynx.setupRatioLabels = function() {

  var statDivs = document.querySelectorAll('[class^=\'propertyContent\']');

  for (var i = 0; i < lynx.stats.length; i++) {

    var statName = lynx.stats[i];

    var statDiv = statDivs[i];

    var ratioLabel = document.createElement('div');
    ratioLabel.className = 'lynx';

    statDiv.appendChild(ratioLabel);

    lynx.ratioLabels[statName] = ratioLabel;

  }

};

lynx.swapGyms = async function(gymToUse) {

  var changeResult = await getAction({
    type: 'post',
    action: 'gym.php',
    data: {
      step: 'changeGym',
      gymID: gymToUse
    }
  });

  if (changeResult.success) {

    lynx.currentGym = gymToUse;

    try {

      var labelName = document.querySelector('[class^=\'notificationText\'] b');
      labelName.innerHTML = lynx.gymInfo[gymToUse].name;

      var listLabelEnergy = document.querySelectorAll('[class^=\'description\']');

      for (var label of listLabelEnergy) {
        label.getElementsByTagName('p')[1].innerHTML = lynx.gymInfo[gymToUse].cost + ' energy per train';
      }

      var activeButton = document.querySelector('[class*=\'active\'][class^=\'gymButton\']');

      var activeClass = '';

      for (var classEntry of activeButton.classList) {
        if (!classEntry.indexOf('active')) {
          activeClass = classEntry;
          break;
        }

      }

      activeButton.classList.remove(activeClass);
      document.querySelector('[class*=\'gym-' + gymToUse + '\']').parentElement.classList.add(activeClass);
      
      var logos = document.querySelectorAll('[class^=\'logo\']');
      
      var logo;

      for(var i = 0; i < logos.length; i++) {
        
        if(logos[i].tagName === 'IMG'){
          logo = logos[i];
          break;
        }
        
      }

      var newSrc = logo.src.split('/');
      newSrc[newSrc.length - 1] = gymToUse + '.png';
      logo.src = newSrc.join('/');
      
    } catch(error) {
      console.log(error);
      return new Response(JSON.stringify({
        message: 'Gym changed but failed to update visual elements. Wait for patch.'
      }));
    }

  }

  return new Response(JSON.stringify({
    message: changeResult.message
  }));
};

lynx.runRatioCheck = function() {

  if (!lynx.initialRatioSetup) {
    lynx.setupRatios(lynx.downloadedStats);
    lynx.initialRatioSetup = true;
  }

  lynx.setupRatioLabels();
  lynx.calculateRatios();
};

lynx.setDisable = function() {

  lynx.disableCheckbox = document.createElement('input');
  lynx.disableCheckbox.type = 'checkbox';
  lynx.disableCheckbox.onchange = function() {
    localStorage.setItem('lynxDisableSwap', lynx.disableCheckbox.checked ? 1 : 0);
  }

  lynx.disableCheckbox.checked = !!+localStorage.getItem('lynxDisableSwap');

  var div = document.createElement('div');
  div.className = 'lynx';
  div.appendChild(lynx.disableCheckbox);

  var label = document.createElement('span');
  label.innerHTML = 'Disable auto gym switch';

  div.appendChild(label);

  document.getElementById('gymroot').appendChild(div);

};

(function() {

  'use strict';

  var lynxElements = document.getElementsByClassName('lynx');

  while (lynxElements.length) {
    lynxElements[0].remove();
  }

  var focusCallback = function(event) {

    if (lynx.domReady && !document.getElementById('specialistGyms')) {
      lynx.runRatioCheck();
      lynx.removedFocusCallback = true;
      removeEventListener('focus', focusCallback);
    }

  };

  addEventListener('focus', focusCallback);

  var originalFetch = window.fetch;

  window.fetch = async function(...args) {

    if (!args[0].indexOf('/gym.php?step=train') && !lynx.disableCheckbox.checked) {

      var stat = JSON.parse(args[1].body).stat.substring(0, 3);

      var gymToUse;
      var gymList = lynx.picks[stat];
      for (var gym of gymList) {

        if (gym.id > 24 && gym.id < 32) {

          var isLocked = false;
          for (var classEntry of document.querySelector('[class*=\'gym-' + gym.id + '\']').parentElement.classList) {
            if (!classEntry.indexOf('locked')) {
              isLocked = true;
              break;
            }

          }

          if (!isLocked) {
            gymToUse = gym.id;
            break;
          }
        } else {
          gymToUse = gym.id;
          break;
        }

      }

      if (gymToUse && gymToUse !== lynx.currentGym) {
        return await lynx.swapGyms(gymToUse);
      }

    }

    var result = await originalFetch(...args);

    var jsonData;

    if (!lynx.booted && !args[0].indexOf('/gym.php?step=getInitialGymInfo')) {

      lynx.booted = true;
      jsonData = await result.clone().json();

      lynx.downloadedStats = jsonData.stats;

      var observer = new MutationObserver(function(mutationList, observer) {

        for (var event of mutationList) {

          // This does not suffice for when the player doesn't confirm the gym
          // change
          if (event.target.tagName === 'DIV' && !event.target.className.indexOf('message') && event.addedNodes.length && !event.addedNodes[0].className.indexOf('messageWrapper')) {

            if (!lynx.disableCheckbox) {
              lynx.setDisable();
            }

            if (document.getElementById('specialistGyms')) {
              return observer.disconnect();
            }

            if (!document.hidden) {
              lynx.runRatioCheck();

              if (!lynx.removedFocusCallback) {
                lynx.removedFocusCallback = true;
                removeEventListener('focus', focusCallback);
              }
            } else {
              lynx.domReady = true;
            }
            break;
          }
        }

      });

      observer.observe(document.getElementsByTagName('body')[0], {
        childList: true,
        subtree: true
      });

      lynx.pickGyms(jsonData.gyms);

    } else if (!args[0].indexOf('/gym.php?step=changeGym') || !args[0].indexOf('/gym.php?step=purchaseMembership')) {

      jsonData = await result.clone().json();

      if (jsonData.success) {

        lynx.currentGym = JSON.parse(args[1].body).gymID;

        if (!lynx.gymInfo[lynx.currentGym].checked) {
          lynx.checkGym(lynx.currentGym);

          for (var sortStat in lynx.picks) {
            lynx.picks[sortStat].sort(lynx.sortGyms);
          }
        }
      }
    } else if (lynx.ratioSetup && !args[0].indexOf('/gym.php?step=train')) {

      jsonData = await result.clone().json();

      if (jsonData.success) {
        // The data will contain gymsStatuses array, objects with id and status
        // when locking status will be lockedPurchased, when unlocking again
        // status will be available
        // Is this better than checking the button each time?
        lynx.currentStats[jsonData.stat.name.substring(0, 3)] = +jsonData.stat.newValue.replace(/,/g, '');
        lynx.calculateRatios();
      }
    }

    return result;

  };

})();