Raw Source
brother-torn / Kama Motivated Resources

// ==UserScript==
// @name         Kama Motivated Resources
// @namespace    http://tampermonkey.net/
// @version      1.4.1
// @description  Makes resources motivating with a balanced tools burst effect
// @author       Brother [2590792], RGiskard [1953860], Xiphias [187717], Kama [3173749]
// @match        https://www.torn.com/gym.php*
// @grant        none
// @run-at       document-idle
// @require      https://code.jquery.com/jquery-latest.js
// @license      AGPL-3.0-only
// @downloadURL https://update.greasyfork.org/scripts/573641/Kama%20Motivated%20Resources.user.js
// @updateURL https://update.greasyfork.org/scripts/573641/Kama%20Motivated%20Resources.meta.js
// ==/UserScript==

(function () {
  "use strict";

  const EMOJIS = {
    strength: ['๐Ÿ”จ', '๐Ÿช“', '๐Ÿ‹๏ธ', '๐Ÿ’ช', '๐Ÿ”ง'],
    speed: ['๐Ÿ‘Ÿ', 'โฑ๏ธ', 'โšก', '๐Ÿšฒ', '๐ŸŽฏ', '๐Ÿชƒ'],
    defense: ['๐Ÿ›ก๏ธ', '๐Ÿงฑ', '๐Ÿข', '๐Ÿงค', '๐ŸฅŠ'],
    dexterity: ['๐Ÿ†', '๐Ÿฅ’', '๐ŸŒ', '๐Ÿ', '๐Ÿ”ฉ'],
    gross: ['๐Ÿคข', '๐Ÿคฎ', '๐Ÿ’ฉ', '๐Ÿ—‘๏ธ', '๐Ÿคก', '๐Ÿ“‰']
  };

  function isBadTrain(btn) {
    const li = btn.closest('li');
    if (!li) return false;

    const statusSpan = li.querySelector('.gymstatus.t-red.bold');
    if (!statusSpan) return false;

    return statusSpan.textContent.toLowerCase().includes('too high!');
  }

  function createToolBurst(startX, startY, emojiSet) {
    const dropCount = 40;
    const particles = [];

    for (let i = 0; i < dropCount; i++) {
      const tool = document.createElement('div');

      tool.innerText = emojiSet[Math.floor(Math.random() * emojiSet.length)];
      tool.style.position = 'fixed';
      tool.style.left = '0px';
      tool.style.top = '0px';
      tool.style.fontSize = (Math.random() * 16 + 20) + 'px';
      tool.style.zIndex = '9999999';
      tool.style.pointerEvents = 'none';
      tool.style.userSelect = 'none';

      document.body.appendChild(tool);

      const angle = Math.random() * Math.PI * 2;
      const velocity = (Math.random() * 12 + 8) * 0.75;

      particles.push({
        el: tool,
        x: startX,
        y: startY,
        vx: Math.cos(angle) * velocity,
        vy: (Math.sin(angle) * velocity) - 9,
        rot: Math.random() * 360,
        rotV: (Math.random() - 0.5) * 20
      });
    }

    function animate() {
      let active = false;

      for (let i = particles.length - 1; i >= 0; i--) {
        const p = particles[i];

        p.vy += 0.6;
        p.x += p.vx;
        p.y += p.vy;
        p.rot += p.rotV;

        p.el.style.transform = `translate3d(${p.x}px, ${p.y}px, 0) rotate(${p.rot}deg)`;

        if (p.y > window.innerHeight + 50) {
          p.el.remove();
          particles.splice(i, 1);
        } else {
          active = true;
        }
      }

      if (active) {
        requestAnimationFrame(animate);
      }
    }

    requestAnimationFrame(animate);
  }

  document.addEventListener("click", (e) => {
    const btn = e.target.closest?.(".torn-btn");
    if (!btn) return;

    const aria = (btn.getAttribute("aria-label") || "").toLowerCase();
    let statName = "";
    let selectedEmojis = [];

    if (aria.includes("train strength")) { statName = "strength"; selectedEmojis = EMOJIS.strength; }
    else if (aria.includes("train speed")) { statName = "speed"; selectedEmojis = EMOJIS.speed; }
    else if (aria.includes("train defense")) { statName = "defense"; selectedEmojis = EMOJIS.defense; }
    else if (aria.includes("train dexterity")) { statName = "dexterity"; selectedEmojis = EMOJIS.dexterity; }

    if (!statName) return;

    if (isBadTrain(btn)) {
        selectedEmojis = EMOJIS.gross;
    }

    let x = e.clientX, y = e.clientY;
    if (!Number.isFinite(x) || !Number.isFinite(y)) {
      const r = btn.getBoundingClientRect();
      x = r.left + r.width / 2;
      y = r.top + r.height / 2;
    }

    createToolBurst(x, y, selectedEmojis);
  }, true);
})();

var statSafeDistance = localStorage.statSafeDistance;
if (statSafeDistance === null) {
    statSafeDistance = 1000000;
}

jQuery.noConflict(true)(document).ready(function($) {
    if (!window.location.href.includes("gym.php")) return;

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

    var FormatAbbreviatedNumber = function(number, maxFractionDigits) {
        var abbreviations = ['', 'k', 'm', 'b', '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) {
        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 (["strength", "defense", "speed", "dexterity"].includes(statName)) {
                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 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() {
        const types = [defenseDexterityGymKeyValue, strengthSpeedGymKeyValue, strengthComboGymKeyValue, defenseComboGymKeyValue, speedComboGymKeyValue, dexterityComboGymKeyValue, strengthGymKeyValue, defenseGymKeyValue, speedGymKeyValue, dexterityGymKeyValue];
        const found = types.find(t => t.value == localStorage.specialistGymType);
        return found || 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 $ratioDisplay = $('<span>').css({
        'margin-left': '10px',
        'font-size': '12px',
        'color': '#888',
        'font-weight': 'bold'
    });

    function updateRatioText() {
        var val = $specialistGymBuild.val();
        var text = "";
        switch (val) {
            case 'balboas': text = "(Def+Dex > Str+Spd by 25%)"; break;
            case 'frontline': text = "(Str+Spd > Def+Dex by 25%)"; break;
            case 'frontlinegym3000': text = "(Str > 2nd stat by 25% & Str+Spd > Def+Dex by 25%)"; break;
            case 'balboasisoyamas': text = "(Def > 2nd stat by 25% & Def+Dex > Str+Spd by 25%)"; break;
            case 'frontlinetotalrebound': text = "(Spd > 2nd stat by 25% & Str+Spd > Def+Dex by 25%)"; break;
            case 'balboaselites': text = "(Dex > 2nd stat by 25% & Def+Dex > Str+Spd by 25%)"; break;
            case 'gym3000': text = "(Str > 2nd stat by 25%)"; break;
            case 'isoyamas': text = "(Def > 2nd stat by 25%)"; break;
            case 'totalrebound': text = "(Spd > 2nd stat by 25%)"; break;
            case 'elites': text = "(Dex > 2nd stat by 25%)"; break;
            default: text = "";
        }
        $ratioDisplay.text(text);
    }

    var $specialistGymBuild = $('<select>', {'class': 'vinkuun-enemeyDifficulty'}).css('margin-top', '10px').on('change', function() {
        localStorage.specialistGymType = $specialistGymBuild.val();
        updateRatioText();
    });

    const options = [noBuildKeyValue, defenseDexterityGymKeyValue, strengthSpeedGymKeyValue, strengthComboGymKeyValue, defenseComboGymKeyValue, speedComboGymKeyValue, dexterityComboGymKeyValue, strengthGymKeyValue, defenseGymKeyValue, speedGymKeyValue, dexterityGymKeyValue];
    options.forEach(opt => $specialistGymBuild.append($('<option>', opt)));

    localStorage.specialistGymType = GetStoredGymKeyValuePair().value;
    $specialistGymBuild.val(GetStoredGymKeyValuePair().value);

    $bottomDiv.append($specialistGymBuild);
    $bottomDiv.append($ratioDisplay);
    $hanksRatioDiv.append($bottomDiv);
    $('#gymroot').append($hanksRatioDiv);

    updateRatioText();

    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) {
            $statContainers.each(function(index, element) {
                $(element).find('.gymstatus').remove();
            });
            return;
        }

        var isComboGymOnlyRatio = [defenseDexterityGymKeyValue.value, strengthSpeedGymKeyValue.value].includes(localStorage.specialistGymType);
        var isComboGymCombinedRatio = [strengthComboGymKeyValue.value, defenseComboGymKeyValue.value, speedComboGymKeyValue.value, dexterityComboGymKeyValue.value].includes(localStorage.specialistGymType);
        var isSingleGymRatio = [strengthGymKeyValue.value, defenseGymKeyValue.value, speedGymKeyValue.value, dexterityGymKeyValue.value].includes(localStorage.specialistGymType);

        var minPrimaryComboSum = 0;
        var maxSecondaryComboSum = 0;
        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 {
            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) {
                                distanceToMax = distanceToComboGymMax;
                                if (distanceToMax < 0) {
                                    statIdentifierString = GetStatAbbreviation(comboGymKeyValuePair.secondarystat1).capitalizeFirstLetter() + ' + ' + GetStatAbbreviation(comboGymKeyValuePair.secondarystat2);
                                }
                            } else {
                                distanceToMax = distanceFromSpecialistGymMin;
                            }
                        }
                    } else if (stat == comboGymKeyValuePair.stat1 || stat == comboGymKeyValuePair.stat2) {
                        distanceToMax = distanceToSpecialistGymMax;
                    } else {
                        distanceToMax = Math.min(distanceToSpecialistGymMax, distanceToComboGymMax);
                        if (distanceToComboGymMax < distanceToSpecialistGymMax && distanceToMax < 0) {
                            statIdentifierString = GetStatAbbreviation(comboGymKeyValuePair.secondarystat1).capitalizeFirstLetter() + ' + ' + GetStatAbbreviation(comboGymKeyValuePair.secondarystat2);
                        }
                    }

                    if (stat == primaryGymKeyValuePair.stat && distanceFromSpecialistGymMin > 0) {
                        gymStatus = '<span class="gymstatus t-red bold">' + statIdentifierString + ' is ' + FormatAbbreviatedNumber(distanceFromSpecialistGymMin, 1) + ' too low!</span>';
                    } else if (distanceToMax < 0) {
                        if (stat == primaryGymKeyValuePair.stat && isComboGymCombinedRatio) {
                            gymStatus = '<span class="gymstatus t-green">' + statIdentifierString + ' is ' + FormatAbbreviatedNumber(-distanceToMax, 1) + ' above the limit.</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__"]');
                $statInfoDiv.find('.gymstatus').remove();
                $statInfoDiv.append(gymStatus);
            }
        });

        oldTotal = total;
        oldBuild = currentBuild;
    }, 400);
});

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

function GetStatAbbreviation(statString) {
    const map = { 'strength': 'str', 'defense': 'def', 'speed': 'spd', 'dexterity': 'dex' };
    return map[statString] || statString;
}