Pasthis

Raw | New paste
Never expires.
// ==UserScript==
// @name         Torn Battle Stat Builds
// @namespace    http://tampermonkey.net/
// @version      2025-02-01
// @description  Display current stats and target percentages based on build selection in Torn, with custom builds, storage, and conditional highlighting.
// @author       kernel_panic [3572165]
// @match        https://www.torn.com/gym.php
// @icon         https://www.torn.com/favicon.ico
// @grant        GM_addStyle
// ==/UserScript==

GM_addStyle(`
  .torn-stats-container {
    background: #222;
    color: #f0f0f0;
    padding: 10px;
    border-radius: 8px;
    margin: 10px auto;
    text-align: center;
    font-family: Arial, sans-serif;
    font-size: 14px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    width: 80%;
    max-width: 600px;
  }
  .torn-stats-container span {
    font-weight: bold;
  }
  .build-dropdown {
    margin-bottom: 10px;
  }
  .stat-row {
    padding: 5px 0;
    color: #94d82d;
  }
  .stat-row.low {
    color: #cc7032;
  }
  .stat-row.high {
    color: #cccc32;
  }
  .stat-message {
    font-size: 12px;
    color: #ddd;
    margin-top: 5px;
  }
  .add-build-form {
    display: none;
    margin: 10px 0;
  }
  .add-build-form input {
    width: 100%;
    padding: 8px 12px;
    margin: 5px 0;
    font-size: 14px;
    border: 1px solid #ccc;
    border-radius: 4px;
    border-color: #555;
    background-color: #000;
    color: #f0f0f0;
    box-sizing: border-box;
}
.add-build-form input:focus {
    outline: none;
    border-color: #a5d8ff;
    box-shadow: 0 0 3px #a5d8ff;
}

  .add-build-form.visible {
    display: block;
  }
  .form-row {
    margin: 5px 0;
  }
  .add-build-button,
  .form-button {
    background: linear-gradient(to bottom, #000000 0%, #111111 5%, #333333 25%, #444444 50%, #333333 75%, #111111 95%, #000000 100%);
    color: #fff;
    padding: 10px 20px;
    font-size: 16px;
    font-weight: bold;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
    border: 1px solid #000;
    border-radius: 12px;
    box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.1), 0 2px 4px rgba(0, 0, 0, 0.5);
    cursor: pointer;
    display: inline-block;
    text-align: center;
    min-width: 120px;
    transition: all 0.2s ease-in-out;
    margin: 10px 0 0 0;
  }
  .add-build-button:hover,
  .form-button:hover {
    background: linear-gradient(to bottom, #444444 0%, #333333 25%, #222222 50%, #333333 75%, #444444 100%);
    border-color: #111;
    box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.2), 0 3px 6px rgba(0, 0, 0, 0.7);
  }
  .add-build-btn:active,
  .form-button:active {
    background: linear-gradient(to bottom, #333333 0%, #111111 100%);
    box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.8);
    transform: translateY(1px);
  }
  .add-build-btn:disabled,
  .form-button:disabled {
    background: linear-gradient(to bottom, #888888 0%, #666666 100%);
    color: #ccc;
    cursor: not-allowed;
    box-shadow: none;
}

  .form-button.discard {
    background: none;
    color: #74c0fc;
    padding: 0;
    font-size: 14px;
    font-weight: normal;
    text-decoration: none;
    border: none;
    box-shadow: none;
    cursor: pointer;
    display: inline;
    text-align: center;
}

  .form-button.discard:hover {
    color: #a5d8ff;
  }
  #buildSelector {
    width: 100%;
    padding: 8px 0;  /* Remove left/right padding for text centering */
    margin: 5px 0;
    font-size: 14px;
    text-align: center;  /* Center the selected text */
    border: 1px solid #ccc;
    border-radius: 4px;
    background-color: #333;
    color: #f0f0f0;
    box-sizing: border-box;
    appearance: none;
    background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23ccc"><path d="M7 10l5 5 5-5z"/></svg>');
    background-repeat: no-repeat;
    background-position: right 12px center;
    background-size: 16px;
    cursor: pointer;
}

#buildSelector:focus {
    outline: none;
    border-color: #a5d8ff;
    box-shadow: 0 0 3px #a5d8ff;
}

#buildSelector option {
    background-color: #222;
    color: #f0f0f0;
    text-align: center;  /* Center text inside options */
}

/* Optional hover effect for dropdown options */
#buildSelector option:hover {
    background-color: #a5d8ff;
    color: white;
}

`);

(function () {
  "use strict";

  const tolerance = 0.1;
  const localStorageKey = "tornCustomBuilds";
  const selectedBuildKey = "selectedBuild";

  const defaultBuilds = {
    Balanced: { strength: 25.0, defense: 25.0, speed: 25.0, dexterity: 25.0 },
    "Hank's Ratio (High Defense/Low Dexterity)": {
      strength: 27.78,
      defense: 34.72,
      speed: 27.78,
      dexterity: 9.72,
    },
    "Hank's Ratio (High Defense/Low Dexterity)": {
      strength: 27.78,
      defense: 34.72,
      speed: 27.78,
      dexterity: 9.72,
    },
    "Hank's Ratio (Low Defense/High Dexterity)": {
      strength: 27.78,
      defense: 9.72,
      speed: 27.78,
      dexterity: 34.72,
    },
    "Baldr's Ratio (Strength/Speed)": {
      strength: 30.86,
      defense: 22.22,
      speed: 24.68,
      dexterity: 22.22,
    },
    "Baldr's Ratio (Speed/Strength)": {
      strength: 24.68,
      defense: 22.22,
      speed: 30.86,
      dexterity: 22.22,
    },
    "Baldr's Ratio (Defense/Dexterity)": {
      strength: 22.22,
      defense: 30.86,
      speed: 22.22,
      dexterity: 24.68,
    },
    "Baldr's Ratio (Dexterity/Defense)": {
      strength: 22.22,
      defense: 24.68,
      speed: 22.22,
      dexterity: 30.86,
    },
  };

  function observeGymStats() {
    const gymRoot = document.getElementById("gymroot");
    if (!gymRoot) {
      console.error("Gym root element not found.");
      return;
    }

    const observer = new MutationObserver(() => {
      const listItems = gymRoot.querySelectorAll("ul li");
      if (listItems.length === 4) {
        const stats = calculateStats(listItems);
        displayStats(stats);
      }
    });

    observer.observe(gymRoot, { childList: true, subtree: true });
  }

  function calculateStats(listItems) {
    const strength = extractStatValue(listItems[0].textContent, "Strength");
    const defense = extractStatValue(listItems[1].textContent, "Defense");
    const speed = extractStatValue(listItems[2].textContent, "Speed");
    const dexterity = extractStatValue(listItems[3].textContent, "Dexterity");

    const total = strength + defense + speed + dexterity;
    return {
      strength: formatNumber(strength),
      defense: formatNumber(defense),
      speed: formatNumber(speed),
      dexterity: formatNumber(dexterity),
      strengthPercentage: ((strength / total) * 100).toFixed(2),
      defensePercentage: ((defense / total) * 100).toFixed(2),
      speedPercentage: ((speed / total) * 100).toFixed(2),
      dexterityPercentage: ((dexterity / total) * 100).toFixed(2),
      total: formatNumber(total),
    };
  }

  function extractStatValue(text, statName) {
    const regex = new RegExp(`${statName}\\s*([\\d,\\.]+)`);
    const match = text.match(regex);
    return match ? parseFloat(match[1].replace(/,/g, "")) : NaN;
  }

  function formatNumber(value) {
    return parseFloat(value).toLocaleString("en-US", {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    });
  }

  function loadBuilds() {
    const storedBuilds =
      JSON.parse(localStorage.getItem(localStorageKey)) || {};
    return { ...defaultBuilds, ...storedBuilds };
  }

  function saveBuilds(builds) {
    const customBuilds = Object.fromEntries(
      Object.entries(builds).filter(([key]) => !(key in defaultBuilds))
    );
    localStorage.setItem(localStorageKey, JSON.stringify(customBuilds));
  }

  function loadSelectedBuild() {
    return localStorage.getItem(selectedBuildKey) || "Balanced";
  }

  function saveSelectedBuild(build) {
    localStorage.setItem(selectedBuildKey, build);
  }

  function displayStats(stats) {
    const userData = document.getElementById("torn-user")?.value;
    if (!userData) {
      console.warn("User data not found.");
      return;
    }

    let container = document.getElementById("statsContainer");
    const builds = loadBuilds();

    if (!container) {
      const user = JSON.parse(userData);

      container = document.createElement("div");
      container.className = "torn-stats-container";
      container.id = "statsContainer";

      const buildOptions = Object.keys(builds)
        .map((build) => `<option value="${build}">${build}</option>`)
        .join("");

      container.innerHTML = `
            <h4>Builds for <span>${user.playername}</span></h4>
            <div class="build-dropdown">
                <select id="buildSelector">${buildOptions}</select>
            </div>
            <div id="statsDisplay"></div>
            <button class="add-build-button" id="showAddBuildForm">ADD BUILD</button>
            <form class="add-build-form" id="addBuildForm">
                <div class="form-row"><input type="text" id="buildName" placeholder="Build Name" required /></div>
                <div class="form-row"><input type="number" id="strengthInput" placeholder="Strength %" step="0.01" required /></div>
                <div class="form-row"><input type="number" id="defenseInput" placeholder="Defense %" step="0.01" required /></div>
                <div class="form-row"><input type="number" id="speedInput" placeholder="Speed %" step="0.01" required /></div>
                <div class="form-row"><input type="number" id="dexterityInput" placeholder="Dexterity %" step="0.01" required /></div>
                <button type="button" class="form-button" id="createBuild">CREATE</button>
                <button type="button" class="form-button discard" id="discardBuild">discard</button>
            </form>
        `;

      document.body.insertBefore(container, document.body.firstChild);

      setupEventListeners(stats, builds);
    }

    const buildSelector = document.getElementById("buildSelector");
    buildSelector.value = loadSelectedBuild();

    updateStatsDisplay(stats, buildSelector.value, builds);
  }
  function setupEventListeners(stats, builds) {
    const buildSelector = document.getElementById("buildSelector");

    buildSelector.addEventListener("change", (e) => {
      saveSelectedBuild(e.target.value);
      updateStatsDisplay(stats, e.target.value, builds);
    });

    document
      .getElementById("showAddBuildForm")
      .addEventListener("click", () => {
        document.getElementById("addBuildForm").classList.add("visible");
      });

    document.getElementById("createBuild").addEventListener("click", () => {
      const newBuild = {
        strength: parseFloat(document.getElementById("strengthInput").value),
        defense: parseFloat(document.getElementById("defenseInput").value),
        speed: parseFloat(document.getElementById("speedInput").value),
        dexterity: parseFloat(document.getElementById("dexterityInput").value),
      };

      const buildName = document.getElementById("buildName").value.trim();
      const totalPercentage = Object.values(newBuild).reduce(
        (sum, val) => sum + val,
        0
      );

      if (!buildName) {
        alert("Your new build must have a name");
        return;
      }

      if (totalPercentage !== 100) {
        alert("The total percentage of all stats must equal 100%.");
        return;
      }

      builds[buildName] = newBuild;
      saveBuilds(builds);
      updateBuildDropdown(buildSelector, builds);
      resetAndHideForm();
    });

    document
      .getElementById("discardBuild")
      .addEventListener("click", resetAndHideForm);

    function resetAndHideForm() {
      document.getElementById("addBuildForm").reset();
      document.getElementById("addBuildForm").classList.remove("visible");
    }
  }

  function updateBuildDropdown(selector, builds) {
    selector.innerHTML = Object.keys(builds)
      .map((build) => `<option value="${build}">${build}</option>`)
      .join("");
  }

  function updateStatsDisplay(stats, build, builds) {
    const targetStats = builds[build];
    const statsDisplay = document.getElementById("statsDisplay");

    const rows = [
      createStatRow(
        "Strength",
        stats.strength,
        stats.strengthPercentage,
        targetStats.strength
      ),
      createStatRow(
        "Defense",
        stats.defense,
        stats.defensePercentage,
        targetStats.defense
      ),
      createStatRow(
        "Speed",
        stats.speed,
        stats.speedPercentage,
        targetStats.speed
      ),
      createStatRow(
        "Dexterity",
        stats.dexterity,
        stats.dexterityPercentage,
        targetStats.dexterity
      ),
    ];

    statsDisplay.innerHTML = rows.join("");
    statsDisplay.innerHTML += `<p>Total: <span>${stats.total}</span></p>`;
  }

  function createStatRow(
    statName,
    statValue,
    currentPercentage,
    targetPercentage
  ) {
    const current = parseFloat(currentPercentage);
    let rowClass = "";
    let message = "";

    if (current < targetPercentage - tolerance) {
      rowClass = "low";
      message = "Increase this stat to meet the target.";
    } else if (current > targetPercentage + tolerance) {
      rowClass = "high";
      message = "This stat is above the target. Focus on other stats.";
    }

    return `
      <div class="stat-row ${rowClass}">
        <p>${statName}: <span>${statValue} (${currentPercentage}%)</span> | Target: <span>${targetPercentage}% ± ${tolerance}</span></p>
        <p class="stat-message">${message}</p>
      </div>
    `;
  }

  window.addEventListener("load", observeGymStats);
})();