Custom Gym Ratios

Monitors battle stat ratios and provides warnings if they approach levels that would preclude access to special gyms

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Custom Gym Ratios
// @namespace    ingine
// @version      0.0.1
// @description  Monitors battle stat ratios and provides warnings if they approach levels that would preclude access to special gyms
// @author       ingine [3255609]
// @include      *.torn.com/gym.php*
// @require      http://code.jquery.com/jquery-latest.js
// @grant        none
// @license      GNU
// ==/UserScript==

// Based off of Torn Gym Pony by Zanoab (http://puu.sh/jFtro/1af393771e.user.js).

// Maximum amount below the stat limit another stat can be before we start warning the player.
var statSafeDistance = localStorage.statSafeDistance;
if (statSafeDistance === null) {
  statSafeDistance = 1000000;
}

// A second method, "Baldr's Ratio", is in this code, but the ability to select it has been
// deliberately excluded for public release. This has been done for clarity, as there is
// no accompanying information about what this ratio is. Those who would like to use it,
// which unlocks a specialty gym that is one of the same stats as a combo gym
// (e.g.: Frontline Fitness (str/spd) and Gym 3000 (str), can uncomment the lines of code
// adding those options to $specialistGymBuild.

jQuery
  .noConflict(true)(document)
  .ready(function ($) {
    var cleanNumber = function (a) {
      return Number(a.replace(/[$,]/g, "").trim());
    };

    /**
     * Formats a number into an abbreviated string with an appropriate trailing descriptive unit
     * up to 't' for trillion.
     * @param {float} number the number to be formatted
     * @param {int} maxFractionDigits the maximum number of fractional digits to display
     * @returns a string representing the number, abbreviated if appropriate
     **/
    var FormatAbbreviatedNumber = function (number, maxFractionDigits) {
      var abbreviations = [];
      abbreviations[0] = "";
      abbreviations[1] = "k";
      abbreviations[2] = "m";
      abbreviations[3] = "b";
      abbreviations[4] = "t";

      var outputNumber = number;
      var abbreviationIndex = 0;
      for (
        ;
        outputNumber >= 1000 && abbreviationIndex < abbreviations.length;
        ++abbreviationIndex
      ) {
        outputNumber = outputNumber / 1000;
      }

      return (
        outputNumber.toLocaleString("EN", {
          maximumFractionDigits: maxFractionDigits,
        }) + abbreviations[abbreviationIndex]
      );
    };

    var getStats = function ($doc) {
      // August 25, 2024: Fix provided by Bennie [2668825] for change in gym page HTML.
      var stats = {};
      $doc = $($doc || document);

      $doc
        .find(
          'h3:contains("Strength"), h3:contains("Defense"), h3:contains("Speed"), h3:contains("Dexterity")'
        )
        .each(function () {
          var statName = $(this).text().toLowerCase();
          var statValue = cleanNumber($(this).siblings("span").first().text());

          if (
            statName === "strength" ||
            statName === "defense" ||
            statName === "speed" ||
            statName === "dexterity"
          ) {
            stats[statName] = statValue;
          }
        });

      return stats;
    };

    var noBuildKeyValue = { value: "none", text: "No specialty gyms" };
    var defenseDexterityGymKeyValue = {
      value: "balboas",
      text: "Defense and dexterity specialist",
      stat1: "defense",
      stat2: "dexterity",
      secondarystat1: "strength",
      secondarystat2: "speed",
    };
    var strengthSpeedGymKeyValue = {
      value: "frontline",
      text: "Strength and speed specialist",
      stat1: "strength",
      stat2: "speed",
      secondarystat1: "defense",
      secondarystat2: "dexterity",
    };
    var customDefenseBuildKeyValue = {
      value: "customdefense",
      text: "Custom Defense Build (Def > Str > Spd > Dex)",
      stat: "defense",
      secondarystat: "strength",
      combogym: strengthSpeedGymKeyValue,
    };
    var strengthComboGymKeyValue = {
      value: "frontlinegym3000",
      text: "Strength combo specialist (Baldr's Ratio)",
      stat: "strength",
      combogym: strengthSpeedGymKeyValue,
    };
    var defenseComboGymKeyValue = {
      value: "balboasisoyamas",
      text: "Defense combo specialist (Baldr's Ratio)",
      stat: "defense",
      combogym: defenseDexterityGymKeyValue,
    };
    var speedComboGymKeyValue = {
      value: "frontlinetotalrebound",
      text: "Speed combo specialist (Baldr's Ratio)",
      stat: "speed",
      combogym: strengthSpeedGymKeyValue,
    };
    var dexterityComboGymKeyValue = {
      value: "balboaselites",
      text: "Dexterity combo specialist (Baldr's Ratio)",
      stat: "dexterity",
      combogym: defenseDexterityGymKeyValue,
    };
    var strengthGymKeyValue = {
      value: "gym3000",
      text: "Strength specialist (Hank's Ratio)",
      stat: "strength",
      combogym: defenseDexterityGymKeyValue,
    };
    var defenseGymKeyValue = {
      value: "isoyamas",
      text: "Defense specialist (Hank's Ratio)",
      stat: "defense",
      combogym: strengthSpeedGymKeyValue,
    };
    var speedGymKeyValue = {
      value: "totalrebound",
      text: "Speed specialist (Hank's Ratio)",
      stat: "speed",
      combogym: defenseDexterityGymKeyValue,
    };
    var dexterityGymKeyValue = {
      value: "elites",
      text: "Dexterity specialist (Hank's Ratio)",
      stat: "dexterity",
      combogym: strengthSpeedGymKeyValue,
    };
    function GetStoredGymKeyValuePair() {
      if (localStorage.specialistGymType == defenseDexterityGymKeyValue.value)
        return defenseDexterityGymKeyValue;
      if (localStorage.specialistGymType == strengthSpeedGymKeyValue.value)
        return strengthSpeedGymKeyValue;
      if (localStorage.specialistGymType == customDefenseBuildKeyValue.value)
        return customDefenseBuildKeyValue;
      if (localStorage.specialistGymType == strengthComboGymKeyValue.value)
        return strengthComboGymKeyValue;
      if (localStorage.specialistGymType == defenseComboGymKeyValue.value)
        return defenseComboGymKeyValue;
      if (localStorage.specialistGymType == speedComboGymKeyValue.value)
        return speedComboGymKeyValue;
      if (localStorage.specialistGymType == dexterityComboGymKeyValue.value)
        return dexterityComboGymKeyValue;
      if (localStorage.specialistGymType == strengthGymKeyValue.value)
        return strengthGymKeyValue;
      if (localStorage.specialistGymType == defenseGymKeyValue.value)
        return defenseGymKeyValue;
      if (localStorage.specialistGymType == speedGymKeyValue.value)
        return speedGymKeyValue;
      if (localStorage.specialistGymType == dexterityGymKeyValue.value)
        return dexterityGymKeyValue;
      return noBuildKeyValue;
    }

    var $hanksRatioDiv = $("<div></div>");
    var $titleDiv = $("<div>", {
      class: "title-black top-round",
      "aria-level": "5",
      text: "Special Gym Ratios",
    }).css("margin-top", "10px");
    $hanksRatioDiv.append($titleDiv);
    var $bottomDiv = $(
      '<div class="bottom-round gym-box cont-gray p10"></div>'
    );
    $bottomDiv.append(
      $('<p class="sub-title">Select desired specialist build:</p>')
    );
    var $specialistGymBuild = $("<select>", {
      class: "vinkuun-enemeyDifficulty",
    })
      .css("margin-top", "10px")
      .on("change", function () {
        localStorage.specialistGymType = $specialistGymBuild.val();
      });
    $specialistGymBuild.append($("<option>", noBuildKeyValue));
    $specialistGymBuild.append($("<option>", customDefenseBuildKeyValue));
    $specialistGymBuild.append($("<option>", defenseDexterityGymKeyValue));
    $specialistGymBuild.append($("<option>", strengthSpeedGymKeyValue));
    $specialistGymBuild.append($("<option>", strengthComboGymKeyValue));
    $specialistGymBuild.append($("<option>", defenseComboGymKeyValue));
    $specialistGymBuild.append($("<option>", speedComboGymKeyValue));
    $specialistGymBuild.append($("<option>", dexterityComboGymKeyValue));
    $specialistGymBuild.append($("<option>", strengthGymKeyValue));
    $specialistGymBuild.append($("<option>", defenseGymKeyValue));
    $specialistGymBuild.append($("<option>", speedGymKeyValue));
    $specialistGymBuild.append($("<option>", dexterityGymKeyValue));
    // Set default to custom defense build if no preference is stored
    if (!localStorage.specialistGymType) {
      localStorage.specialistGymType = customDefenseBuildKeyValue.value;
    }
    localStorage.specialistGymType = GetStoredGymKeyValuePair().value; // In case there is bad data, replace it.
    $specialistGymBuild.val(GetStoredGymKeyValuePair().value);
    $bottomDiv.append($specialistGymBuild);
    $hanksRatioDiv.append($bottomDiv);
    $("#gymroot").append($hanksRatioDiv);

    var oldTotal = 0;
    var oldBuild = "";
    setInterval(function () {
      var stats = getStats();
      var total = 0;
      var highestSecondaryStat = 0;
      for (var stat in stats) {
        total += stats[stat];
        if (
          GetStoredGymKeyValuePair().stat &&
          GetStoredGymKeyValuePair().stat != stat &&
          stats[stat] > highestSecondaryStat
        ) {
          highestSecondaryStat = stats[stat];
        }
      }
      var currentBuild = $specialistGymBuild.val();

      if (
        oldTotal == total &&
        oldBuild == currentBuild &&
        $(".gymstatus").size() != 0
      ) {
        return;
      }

      var $statContainers = $(
        '[class^="gymContent__"], [class*=" gymContent__"]'
      ).find("li");

      if (currentBuild == noBuildKeyValue.value) {
        // Clear the training info in case it exists.
        $statContainers.each(function (index, element) {
          var $statInfoDiv = $(element).find(
            '[class^="description__"], [class*=" description__"]'
          );
          var $insertedElement = $statInfoDiv.find(".gymstatus");
          $insertedElement.remove();
        });
        return;
      }

      var isComboGymOnlyRatio =
        localStorage.specialistGymType == defenseDexterityGymKeyValue.value ||
        localStorage.specialistGymType == strengthSpeedGymKeyValue.value;
      var isComboGymCombinedRatio =
        localStorage.specialistGymType == strengthComboGymKeyValue.value ||
        localStorage.specialistGymType == defenseComboGymKeyValue.value ||
        localStorage.specialistGymType == speedComboGymKeyValue.value ||
        localStorage.specialistGymType == dexterityComboGymKeyValue.value;
      var isSingleGymRatio =
        localStorage.specialistGymType == strengthGymKeyValue.value ||
        localStorage.specialistGymType == defenseGymKeyValue.value ||
        localStorage.specialistGymType == speedGymKeyValue.value ||
        localStorage.specialistGymType == dexterityGymKeyValue.value;
      var isCustomDefenseRatio =
        localStorage.specialistGymType == customDefenseBuildKeyValue.value;

      // The combined total of the primary stats must be 25% higher than the total of the secondary stats.
      var minPrimaryComboSum = 0; // The minimum amount the combined primary stats must be to unlock the gym based on the secondary stat sum.
      var maxSecondaryComboSum = 0; // The maximum amount the combined secondary stats must be to unlock the gym based on the primary stat sum.
      // The primary stat needs to be 25% higher than the second highest stat.
      var minPrimaryStat = 0;
      var maxSecondaryStat = 0;
      var comboGymKeyValuePair = noBuildKeyValue;
      var primaryGymKeyValuePair = noBuildKeyValue;
      if (isComboGymOnlyRatio) {
        comboGymKeyValuePair = GetStoredGymKeyValuePair();
      } else if (isComboGymCombinedRatio || isSingleGymRatio) {
        primaryGymKeyValuePair = GetStoredGymKeyValuePair();
        comboGymKeyValuePair = primaryGymKeyValuePair.combogym;
        minPrimaryStat = highestSecondaryStat * 1.25;
        maxSecondaryStat = stats[primaryGymKeyValuePair.stat] / 1.25;
      } else if (isCustomDefenseRatio) {
        primaryGymKeyValuePair = GetStoredGymKeyValuePair();
        comboGymKeyValuePair = primaryGymKeyValuePair.combogym;
        // For custom defense build, we want defense to be 25% higher than strength (second highest)
        minPrimaryStat = stats[primaryGymKeyValuePair.secondarystat] * 1.25;
        maxSecondaryStat = stats[primaryGymKeyValuePair.stat] / 1.25;
      } else {
        console.debug(
          "Somehow attempted to calculate stat requirements for invalid gym: " +
            GetStoredGymKeyValuePair()
        );
        return;
      }
      minPrimaryComboSum =
        (stats[comboGymKeyValuePair.secondarystat1] +
          stats[comboGymKeyValuePair.secondarystat2]) *
        1.25;
      maxSecondaryComboSum =
        (stats[comboGymKeyValuePair.stat1] +
          stats[comboGymKeyValuePair.stat2]) /
        1.25;

      var distanceFromComboGymMin =
        minPrimaryComboSum -
        stats[comboGymKeyValuePair.stat1] -
        stats[comboGymKeyValuePair.stat2];
      var distanceToComboGymMax =
        maxSecondaryComboSum -
        stats[comboGymKeyValuePair.secondarystat1] -
        stats[comboGymKeyValuePair.secondarystat2];

      $statContainers.each(function (index, element) {
        var $element = $(element);
        var title = $element.find('[class^="title__"], [class*=" title__"]');
        var stat = $element.attr("zStat");
        if (!stat) {
          stat = title.text().toLowerCase();
          $element.attr("zStat", stat);
        }
        if (stats[stat]) {
          var gymStatus;
          var statIdentifierString;
          if (isComboGymOnlyRatio) {
            if (
              stat == comboGymKeyValuePair.stat1 ||
              stat == comboGymKeyValuePair.stat2
            ) {
              statIdentifierString =
                GetStatAbbreviation(
                  comboGymKeyValuePair.stat1
                ).capitalizeFirstLetter() +
                " + " +
                GetStatAbbreviation(comboGymKeyValuePair.stat2);
              if (distanceFromComboGymMin > 0) {
                gymStatus =
                  '<span class="gymstatus t-red bold">' +
                  statIdentifierString +
                  " is " +
                  FormatAbbreviatedNumber(distanceFromComboGymMin, 1) +
                  " too low!</span>";
              } else if (distanceFromComboGymMin < statSafeDistance) {
                gymStatus =
                  '<span class="gymstatus t-red bold">' +
                  statIdentifierString +
                  " is " +
                  FormatAbbreviatedNumber(-distanceFromComboGymMin, 1) +
                  " above the limit.</span>";
              } else {
                gymStatus =
                  '<span class="gymstatus t-green">' +
                  statIdentifierString +
                  " is " +
                  FormatAbbreviatedNumber(-distanceFromComboGymMin, 1) +
                  " above the limit.</span>";
              }
            } else {
              statIdentifierString =
                GetStatAbbreviation(
                  comboGymKeyValuePair.secondarystat1
                ).capitalizeFirstLetter() +
                " + " +
                GetStatAbbreviation(comboGymKeyValuePair.secondarystat2);
              if (distanceToComboGymMax < 0) {
                gymStatus =
                  '<span class="gymstatus t-red bold">' +
                  statIdentifierString +
                  " is " +
                  FormatAbbreviatedNumber(-distanceToComboGymMax, 1) +
                  " too high!</span>";
              } else if (distanceToComboGymMax < statSafeDistance) {
                gymStatus =
                  '<span class="gymstatus t-red bold">' +
                  statIdentifierString +
                  " is " +
                  FormatAbbreviatedNumber(distanceToComboGymMax, 1) +
                  " below the limit.</span>";
              } else {
                gymStatus =
                  '<span class="gymstatus t-green">' +
                  statIdentifierString +
                  " is " +
                  FormatAbbreviatedNumber(distanceToComboGymMax, 1) +
                  " below the limit.</span>";
              }
            }
          } else {
            var distanceFromSpecialistGymMin = minPrimaryStat - stats[stat];
            var distanceToSpecialistGymMax = maxSecondaryStat - stats[stat];

            var distanceToMax = 0;
            statIdentifierString = stat.capitalizeFirstLetter();
            if (stat == primaryGymKeyValuePair.stat) {
              if (distanceFromSpecialistGymMin <= 0) {
                if (isSingleGymRatio || isCustomDefenseRatio) {
                  // Specialist stat for Hank's Gym Ratio is never one of the primary combo stats.
                  // Only set the identifier if we don't already know this stat is too low to unlock its own specific gym.
                  distanceToMax = distanceToComboGymMax;
                  if (distanceToMax < 0) {
                    statIdentifierString =
                      GetStatAbbreviation(
                        comboGymKeyValuePair.secondarystat1
                      ).capitalizeFirstLetter() +
                      " + " +
                      GetStatAbbreviation(comboGymKeyValuePair.secondarystat2);
                  }
                } else {
                  // Specialist stat IS the combo stat; we only care to show how it's doing in relation to the specialist gym.
                  distanceToMax = distanceFromSpecialistGymMin;
                }
              }
            } else if (
              stat == comboGymKeyValuePair.stat1 ||
              stat == comboGymKeyValuePair.stat2
            ) {
              // We don't have to worry about this stat going too high for the combo gym.
              distanceToMax = distanceToSpecialistGymMax;
            } else {
              // This stat is neither the primary stat nor a combo gym stat, so it's limited by both.
              distanceToMax = Math.min(
                distanceToSpecialistGymMax,
                distanceToComboGymMax
              );
              if (
                distanceToComboGymMax < distanceToSpecialistGymMax &&
                distanceToMax < 0
              ) {
                statIdentifierString =
                  GetStatAbbreviation(
                    comboGymKeyValuePair.secondarystat1
                  ).capitalizeFirstLetter() +
                  " + " +
                  GetStatAbbreviation(comboGymKeyValuePair.secondarystat2);
              }
            }

            if (stat == primaryGymKeyValuePair.stat) {
              console.debug(
                stat +
                  " distanceFromSpecialistGymMin: " +
                  distanceFromSpecialistGymMin
              );
              console.debug(
                stat + " distanceToComboGymMax: " + distanceToComboGymMax
              );
            } else if (
              stat == comboGymKeyValuePair.stat1 ||
              stat == comboGymKeyValuePair.stat2
            ) {
              console.debug(
                stat +
                  " distanceToSpecialistGymMax: " +
                  distanceToSpecialistGymMax
              );
              console.debug(
                stat + " distanceFromComboGymMin: " + distanceFromComboGymMin
              );
            } else {
              console.debug(
                stat +
                  " distanceToSpecialistGymMax: " +
                  distanceToSpecialistGymMax
              );
              console.debug(
                stat + " distanceToComboGymMax: " + distanceToComboGymMax
              );
            }
            console.debug(stat + " distanceToMax: " + distanceToMax);

            if (
              stat == primaryGymKeyValuePair.stat &&
              distanceFromSpecialistGymMin > 0
            ) {
              if (isCustomDefenseRatio) {
                gymStatus =
                  '<span class="gymstatus t-red bold">' +
                  statIdentifierString +
                  " is " +
                  FormatAbbreviatedNumber(distanceFromSpecialistGymMin, 1) +
                  " too low for Mr. Isoyamas! Train Defense!</span>";
              } else {
                gymStatus =
                  '<span class="gymstatus t-red bold">' +
                  statIdentifierString +
                  " is " +
                  FormatAbbreviatedNumber(distanceFromSpecialistGymMin, 1) +
                  " too low!</span>";
              }
            } else if (distanceToMax < 0) {
              if (
                stat == primaryGymKeyValuePair.stat &&
                (isComboGymCombinedRatio || isCustomDefenseRatio)
              ) {
                gymStatus =
                  '<span class="gymstatus t-green">' +
                  statIdentifierString +
                  " is " +
                  FormatAbbreviatedNumber(-distanceToMax, 1) +
                  " above the limit.</span>";
              } else {
                if (
                  isCustomDefenseRatio &&
                  stat == primaryGymKeyValuePair.secondarystat
                ) {
                  gymStatus =
                    '<span class="gymstatus t-red bold">' +
                    statIdentifierString +
                    " is " +
                    FormatAbbreviatedNumber(-distanceToMax, 1) +
                    " too high! Will lock you out of Mr. Isoyamas!</span>";
                } else {
                  gymStatus =
                    '<span class="gymstatus t-red bold">' +
                    statIdentifierString +
                    " is " +
                    FormatAbbreviatedNumber(-distanceToMax, 1) +
                    " too high!</span>";
                }
              }
            } else if (distanceToMax < statSafeDistance) {
              gymStatus =
                '<span class="gymstatus t-red bold">' +
                statIdentifierString +
                " is " +
                FormatAbbreviatedNumber(distanceToMax, 1) +
                " below the limit.</span>";
            } else {
              gymStatus =
                '<span class="gymstatus t-green">' +
                statIdentifierString +
                " is " +
                FormatAbbreviatedNumber(distanceToMax, 1) +
                " below the limit.</span>";
            }
          }

          var $statInfoDiv = $element.find(
            '[class^="description__"], [class*=" description__"]'
          );
          var $insertedElement = $statInfoDiv.find(".gymstatus");
          $insertedElement.remove();
          $statInfoDiv.append(gymStatus);
        }
      });
      oldTotal = total;
      oldBuild = currentBuild;
      console.debug("Stat spread updated!");
    }, 400);
  });

String.prototype.capitalizeFirstLetter = function () {
  return this.charAt(0).toUpperCase() + this.slice(1);
};

function GetStatAbbreviation(statString) {
  if (statString == "strength") {
    return "str";
  } else if (statString == "defense") {
    return "def";
  } else if (statString == "speed") {
    return "spd";
  } else if (statString == "dexterity") {
    return "dex";
  }
  return statString;
}