您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A calculator for Torn's gym training, predicting stat gains, time to reach goals, and booster costs. Features a collapsible UI with bonus settings, energy/happiness boosters, and persistent user inputs.
// ==UserScript== // @name TornPDA - Gym Gains Calculator // @namespace http://tampermonkey.net/ // @version 2.53 // @description A calculator for Torn's gym training, predicting stat gains, time to reach goals, and booster costs. Features a collapsible UI with bonus settings, energy/happiness boosters, and persistent user inputs. // @author Jvmie[2094564] // @match https://www.torn.com/gym.php* // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; // Gym data const gyms = { "[H] Gun Shop": { energy: 10, strength: 6.6, speed: 6.4, defense: 6.2, dexterity: 6.2 }, "[L] Premier Fitness": { energy: 5, strength: 2, speed: 2, defense: 2, dexterity: 2 }, "[L] Average Joes": { energy: 5, strength: 2.4, speed: 2.4, defense: 2.8, dexterity: 2.4 }, "[L] Woody's Workout": { energy: 5, strength: 2.8, speed: 3.2, defense: 3, dexterity: 2.8 }, "[L] Beach Bods": { energy: 5, strength: 3.2, speed: 3.2, defense: 3.2, dexterity: 0 }, "[L] Silver Gym": { energy: 5, strength: 3.4, speed: 3.6, defense: 3.4, dexterity: 3.2 }, "[L] Pour Femme": { energy: 5, strength: 3.4, speed: 3.6, defense: 3.6, dexterity: 3.8 }, "[L] Davies Den": { energy: 5, strength: 3.7, speed: 0, defense: 3.7, dexterity: 3.7 }, "[L] Global Gym": { energy: 5, strength: 4, speed: 4, defense: 4, dexterity: 4 }, "[M] Knuckle Heads": { energy: 10, strength: 4.8, speed: 4.4, defense: 4, dexterity: 4.2 }, "[M] Pioneer Fitness": { energy: 10, strength: 4.4, speed: 4.6, defense: 4.8, dexterity: 4.4 }, "[M] Anabolic Anomalies": { energy: 10, strength: 5, speed: 4.6, defense: 5.2, dexterity: 4.6 }, "[M] Core": { energy: 10, strength: 5, speed: 5.2, defense: 5, dexterity: 5 }, "[M] Racing Fitness": { energy: 10, strength: 5, speed: 5.4, defense: 4.8, dexterity: 5.2 }, "[M] Complete Cardio": { energy: 10, strength: 5.5, speed: 5.8, defense: 5.5, dexterity: 5.2 }, "[M] Legs, Bums and Tums": { energy: 10, strength: 0, speed: 5.6, defense: 5.6, dexterity: 5.8 }, "[M] Deep Burn": { energy: 10, strength: 6, speed: 6, defense: 6, dexterity: 6 }, "[H] Apollo Gym": { energy: 10, strength: 6, speed: 6.2, defense: 6.4, dexterity: 6.2 }, "[H] Force Training": { energy: 10, strength: 6.4, speed: 6.6, defense: 6.4, dexterity: 6.8 }, "[H] Cha Cha's": { energy: 10, strength: 6.4, speed: 6.4, defense: 6.8, dexterity: 7 }, "[H] Atlas": { energy: 10, strength: 7, speed: 6.4, defense: 6.4, dexterity: 6.6 }, "[H] Last Round": { energy: 10, strength: 6.8, speed: 6.6, defense: 7, dexterity: 6.6 }, "[H] The Edge": { energy: 10, strength: 6.8, speed: 7, defense: 7, dexterity: 6.8 }, "[H] George's": { energy: 10, strength: 7.3, speed: 7.3, defense: 7.3, dexterity: 7.3 }, "[S] Balboas Gym": { energy: 25, strength: 0, speed: 0, defense: 7.5, dexterity: 7.5 }, "[S] Frontline Fitness": { energy: 25, strength: 7.5, speed: 7.5, defense: 0, dexterity: 0 }, "[S] Gym 3000": { energy: 50, strength: 8, speed: 0, defense: 0, dexterity: 0 }, "[S] Mr. Isoyamas": { energy: 50, strength: 0, speed: 0, defense: 8, dexterity: 0 }, "[S] Total Rebound": { energy: 50, strength: 0, speed: 8, defense: 0, dexterity: 0 }, "[S] Elites": { energy: 50, strength: 0, speed: 0, defense: 0, dexterity: 8 }, "[S] Sports Science Lab": { energy: 25, strength: 9, speed: 9, defense: 9, dexterity: 9 }, "The Jail Gym": { energy: 5, strength: 3.4, speed: 3.4, defense: 4.6, dexterity: 0 } }; const stats = ["Strength", "Speed", "Defense", "Dexterity"]; const bonusPercentages = Array.from({ length: 101 }, (_, i) => i); // 0 to 100 // CSS for the embedded collapsible menu const styles = ` .grok-menu { margin: 10px 0; background: #1a1a1a; border: 1px solid #444; border-radius: 5px; padding: 10px; font-family: "Arial", sans-serif; color: #ccc; } .grok-menu h3 { margin: 0; padding: 10px; background: linear-gradient(to bottom, #333, #222); cursor: pointer; border-radius: 3px; font-size: 16px; font-weight: bold; color: #ddd; border: 1px solid #555; transition: background 0.2s ease; } .grok-menu h3:hover { background: linear-gradient(to bottom, #444, #333); } .grok-content { display: none; padding: 10px; } .grok-content.show { display: block; } .grok-menu label { display: block; margin: 8px 0; font-size: 14px; color: #ddd; } .grok-menu input, .grok-menu select { width: 100%; padding: 5px; margin-top: 2px; border: 1px solid #444; border-radius: 3px; background: #333; color: #fff; box-sizing: border-box; font-size: 14px; } .grok-menu input:focus, .grok-menu select:focus { outline: none; border-color: #666; } .grok-menu .result { margin-top: 10px; padding: 8px; border-radius: 3px; font-size: 14px; } .grok-menu .result.red { background: #3a1c1c; color: #ff6666; } .grok-menu .result.grey { background: #2a2a2a; color: #bbb; } .grok-menu .button-container { margin-top: 10px; display: flex; gap: 10px; } .grok-menu button { background: #2a2a2a; border: 1px solid #444; border-radius: 3px; padding: 5px 10px; color: #fff; cursor: pointer; font-size: 14px; transition: background 0.2s ease; } .grok-menu button:hover { background: #3a3a3a; } .bonus-menu { margin: 10px 0; background: #1a1a1a; border: 1px solid #444; border-radius: 3px; } .bonus-menu h4 { margin: 0; padding: 8px; background: linear-gradient(to bottom, #333, #222); cursor: pointer; border-radius: 3px; font-size: 14px; font-weight: bold; color: #ddd; border: 1px solid #555; transition: background 0.2s ease; } .bonus-menu h4:hover { background: linear-gradient(to bottom, #444, #333); } .bonus-content { display: none; padding: 8px; } .bonus-content.show { display: block; } .changelog-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); z-index: 1000; display: flex; justify-content: center; align-items: center; } .changelog-box { background: #1a1a1a; border: 1px solid #444; border-radius: 5px; padding: 20px; max-width: 400px; color: #ccc; font-family: "Arial", sans-serif; } .changelog-box h4 { margin: 0 0 10px; color: #fff; } .changelog-box ul { margin: 0 0 20px; padding-left: 20px; } .changelog-box button { background: #2a2a2a; border: 1px solid #444; border-radius: 3px; padding: 5px 10px; color: #fff; cursor: pointer; } .changelog-box button:hover { background: #3a3a3a; } `; // Add styles to the page const styleSheet = document.createElement("style"); styleSheet.textContent = styles; document.head.appendChild(styleSheet); // Find the main content area const contentWrapper = document.querySelector('.content-wrapper') || document.body; if (!contentWrapper) { console.error("Could not find content wrapper to embed calculator."); return; } // Load saved values const savedGym = GM_getValue("gym", "[H] Gun Shop"); const savedStat = GM_getValue("stat", "strength"); const savedEnergyBooster = GM_getValue("energyBooster", "none"); const savedBoosterCount = GM_getValue("boosterCount", "0"); const savedHappy = GM_getValue("happy", "4525"); const savedStatTotal = GM_getValue("statTotal", "234522"); const savedStatGoal = GM_getValue("statGoal", "300000"); const savedEnergy = GM_getValue("energy", "10"); const savedFactionPerk = GM_getValue("factionPerk", "0"); const savedPropertyPerk = GM_getValue("propertyPerk", "0"); const savedEduStatPerk = GM_getValue("eduStatPerk", "0"); const savedEduGenPerk = GM_getValue("eduGenPerk", "0"); const savedJobPerk = GM_getValue("jobPerk", "0"); const savedBookPerk = GM_getValue("bookPerk", "0"); const savedSportsSneakers = GM_getValue("sportsSneakers", "0"); const savedSteroids = GM_getValue("steroids", "0"); const savedEcstasy = GM_getValue("ecstasy", "no"); const savedEroticDVDs = GM_getValue("eroticDVDs", "0"); // Create the menu HTML with saved values const menu = document.createElement("div"); menu.className = "grok-menu"; menu.innerHTML = ` <h3>TornPDA - Gym Gains Calculator</h3> <div class="grok-content"> <label>Gym: <select id="gymSelect"></select> </label> <label>Stat to Train: <select id="statSelect"> ${stats.map(stat => `<option value="${stat.toLowerCase()}">${stat}</option>`).join('')} </select> </label> <label>Energy Booster: <select id="energyBooster"> <option value="none">None</option> <option value="xanax">Xanax (+250 Energy, $880,000, 7 hr cooldown)</option> <option value="energyCan">Energy Can (+20 Energy, $1,166,667, 30 min cooldown)</option> <option value="fhc">Feathery Hotel Coupon (150 Energy, $12,500,000, 24 hr cooldown)</option> <option value="refill">Energy Refill (150 Energy, $1,725,000, No cooldown)</option> </select> </label> <label>Number of Energy Boosters per Day: <input type="number" id="boosterCount" value="${savedBoosterCount}" min="0"> </label> <label>Starting Happy: <input type="number" id="happy" value="${savedHappy}" min="0"></label> <label>Current Stat Total: <input type="number" id="statTotal" value="${savedStatTotal}" min="0"></label> <label>Desired Stat Goal: <input type="number" id="statGoal" value="${savedStatGoal}" min="0"></label> <label>Total Energy to Spend (Initial): <input type="number" id="energy" value="${savedEnergy}" min="0"></label> <div class="bonus-menu"> <h4>Bonuses & Boosters</h4> <div class="bonus-content"> <label>Faction Steadfast (%): <select id="factionPerk"> ${bonusPercentages.map(val => `<option value="${val}">${val}%</option>`).join('')} </select> </label> <label>Property Perks (%): <select id="propertyPerk"> ${bonusPercentages.map(val => `<option value="${val}">${val}%</option>`).join('')} </select> </label> <label>Education (Stat Specific) (%): <select id="eduStatPerk"> ${bonusPercentages.map(val => `<option value="${val}">${val}%</option>`).join('')} </select> </label> <label>Education (General) (%): <select id="eduGeneralPerk"> ${bonusPercentages.map(val => `<option value="${val}">${val}%</option>`).join('')} </select> </label> <label>Job Perks (%): <select id="jobPerk"> ${bonusPercentages.map(val => `<option value="${val}">${val}%</option>`).join('')} </select> </label> <label>Book Perks (%): <select id="bookPerk"> ${bonusPercentages.map(val => `<option value="${val}">${val}%</option>`).join('')} </select> </label> <label>Sports Sneakers (Speed Only, %): <select id="sportsSneakers"> <option value="0">0%</option> <option value="5">5%</option> </select> </label> <label>Steroids Booster (%): <select id="steroids"> <option value="0">0%</option> <option value="20">20%</option> </select> </label> <label>Ecstasy (Doubles Happiness): <select id="ecstasy"> <option value="no">No</option> <option value="yes">Yes</option> </select> </label> <label>Erotic DVDs (Happiness Boost): <select id="eroticDVDs"> <option value="0">0</option> <option value="1">1 (+2500 Happy)</option> <option value="2">2 (+5000 Happy)</option> <option value="3">3 (+7500 Happy)</option> <option value="4">4 (+10000 Happy)</option> </select> </label> </div> </div> <div class="button-container"> <button id="calculateButton">Calculate</button> <button id="copyButton" style="display: none;">Copy Results</button> </div> <div class="result grey">Energy Per Train: <span id="energyPerTrain">-</span></div> <div class="result grey">Number of Trains: <span id="numTrains">-</span></div> <div class="result grey">Bonus Multiplier: <span id="bonusMultiplier">-</span></div> <div class="result red">Predicted Gains (Single Train): <span id="singleGain">-</span></div> <div class="result red">Predicted Gains (Total Initial): <span id="totalGain">-</span></div> <div class="result red">Allowable Error (+/-): <span id="errorMargin">-</span></div> <div class="result grey">Total Energy Per Day: <span id="dailyEnergy">-</span></div> <div class="result grey">Total Cost of Boosters: <span id="boosterCost">-</span></div> <div class="result grey">Days to Reach Goal: <span id="daysToGoal">-</span></div> <div class="result grey">Total Booster Cost to Goal: <span id="totalBoosterCost">-</span></div> </div> `; // Populate gym dropdown and set saved value const gymSelect = menu.querySelector("#gymSelect"); Object.keys(gyms).forEach(gym => { const option = document.createElement("option"); option.value = gym; option.textContent = gym; gymSelect.appendChild(option); }); gymSelect.value = savedGym; // Set saved values for other inputs const statSelect = menu.querySelector("#statSelect"); statSelect.value = savedStat; const energyBoosterSelect = menu.querySelector("#energyBooster"); energyBoosterSelect.value = savedEnergyBooster; const factionPerkSelect = menu.querySelector("#factionPerk"); factionPerkSelect.value = savedFactionPerk; const propertyPerkSelect = menu.querySelector("#propertyPerk"); propertyPerkSelect.value = savedPropertyPerk; const eduStatPerkSelect = menu.querySelector("#eduStatPerk"); eduStatPerkSelect.value = savedEduStatPerk; const eduGenPerkSelect = menu.querySelector("#eduGeneralPerk"); eduGenPerkSelect.value = savedEduGenPerk; const jobPerkSelect = menu.querySelector("#jobPerk"); jobPerkSelect.value = savedJobPerk; const bookPerkSelect = menu.querySelector("#bookPerk"); bookPerkSelect.value = savedBookPerk; const sportsSneakersSelect = menu.querySelector("#sportsSneakers"); sportsSneakersSelect.value = savedSportsSneakers; const steroidsSelect = menu.querySelector("#steroids"); steroidsSelect.value = savedSteroids; const ecstasySelect = menu.querySelector("#ecstasy"); ecstasySelect.value = savedEcstasy; const eroticDVDsSelect = menu.querySelector("#eroticDVDs"); eroticDVDsSelect.value = savedEroticDVDs; // Insert the menu contentWrapper.insertBefore(menu, contentWrapper.firstChild); // Toggle main menu visibility const header = menu.querySelector("h3"); const content = menu.querySelector(".grok-content"); header.addEventListener("click", () => { content.classList.toggle("show"); }); // Toggle bonus menu visibility const bonusHeader = menu.querySelector(".bonus-menu h4"); const bonusContent = menu.querySelector(".bonus-content"); bonusHeader.addEventListener("click", () => { bonusContent.classList.toggle("show"); }); // Save input values on change gymSelect.addEventListener("change", () => GM_setValue("gym", gymSelect.value)); statSelect.addEventListener("change", () => GM_setValue("stat", statSelect.value)); energyBoosterSelect.addEventListener("change", () => GM_setValue("energyBooster", energyBoosterSelect.value)); factionPerkSelect.addEventListener("change", () => GM_setValue("factionPerk", factionPerkSelect.value)); propertyPerkSelect.addEventListener("change", () => GM_setValue("propertyPerk", propertyPerkSelect.value)); eduStatPerkSelect.addEventListener("change", () => GM_setValue("eduStatPerk", eduStatPerkSelect.value)); eduGenPerkSelect.addEventListener("change", () => GM_setValue("eduGeneralPerk", eduGenPerkSelect.value)); jobPerkSelect.addEventListener("change", () => GM_setValue("jobPerk", jobPerkSelect.value)); bookPerkSelect.addEventListener("change", () => GM_setValue("bookPerk", bookPerkSelect.value)); sportsSneakersSelect.addEventListener("change", () => GM_setValue("sportsSneakers", sportsSneakersSelect.value)); steroidsSelect.addEventListener("change", () => GM_setValue("steroids", steroidsSelect.value)); ecstasySelect.addEventListener("change", () => GM_setValue("ecstasy", ecstasySelect.value)); eroticDVDsSelect.addEventListener("change", () => GM_setValue("eroticDVDs", eroticDVDsSelect.value)); const boosterCountInput = menu.querySelector("#boosterCount"); boosterCountInput.addEventListener("change", () => GM_setValue("boosterCount", boosterCountInput.value)); const happyInput = menu.querySelector("#happy"); happyInput.addEventListener("change", () => GM_setValue("happy", happyInput.value)); const statTotalInput = menu.querySelector("#statTotal"); statTotalInput.addEventListener("change", () => GM_setValue("statTotal", statTotalInput.value)); const statGoalInput = menu.querySelector("#statGoal"); statGoalInput.addEventListener("change", () => GM_setValue("statGoal", statGoalInput.value)); const energyInput = menu.querySelector("#energy"); energyInput.addEventListener("change", () => GM_setValue("energy", energyInput.value)); // Calculation function with error handling function calculateGains() { try { const gym = gymSelect.value; const stat = statSelect.value; const gymData = gyms[gym]; if (!gymData) throw new Error("Invalid gym selected"); const energyPerTrain = gymData.energy; const gymDots = gymData[stat]; if (!gymDots) throw new Error("Invalid stat for this gym"); let happy = parseFloat(document.getElementById("happy").value) || 0; let statTotal = parseFloat(document.getElementById("statTotal").value) || 0; const statGoal = parseFloat(document.getElementById("statGoal").value) || statTotal; const totalEnergy = parseFloat(document.getElementById("energy").value) || 0; // Perks const factionPerk = (parseFloat(document.getElementById("factionPerk").value) || 0) / 100; const propertyPerk = (parseFloat(document.getElementById("propertyPerk").value) || 0) / 100; const eduStatPerk = (parseFloat(document.getElementById("eduStatPerk").value) || 0) / 100; const eduGenPerk = (parseFloat(document.getElementById("eduGeneralPerk").value) || 0) / 100; const jobPerk = (parseFloat(document.getElementById("jobPerk").value) || 0) / 100; const bookPerk = (parseFloat(document.getElementById("bookPerk").value) || 0) / 100; const sportsSneakers = (parseFloat(document.getElementById("sportsSneakers").value) || 0) / 100; const steroids = (parseFloat(document.getElementById("steroids").value) || 0) / 100; const ecstasy = document.getElementById("ecstasy").value; const eroticDVDs = parseInt(document.getElementById("eroticDVDs").value) || 0; const energyBooster = document.getElementById("energyBooster").value; let boosterCount = parseInt(document.getElementById("boosterCount").value) || 0; // Apply happiness boosters if (ecstasy === "yes") { happy *= 2; } happy += eroticDVDs * 2500; // Bonus multiplier let bonusMultiplier = (1 + factionPerk) * (1 + propertyPerk) * (1 + eduStatPerk) * (1 + eduGenPerk) * (1 + jobPerk) * (1 + bookPerk) * (1 + steroids); if (stat === "speed") { bonusMultiplier *= (1 + sportsSneakers); } // Number of trains (initial) const numTrains = Math.floor(totalEnergy / energyPerTrain); // Calculate single train gain const initialCoreComponent = (0.00019106 * statTotal) + (0.00226263 * happy) + 0.55; const initialBaseGain = (gymDots * 4) * initialCoreComponent; const singleGain = (initialBaseGain * bonusMultiplier / 147.24) * energyPerTrain; // Iterative calculation for initial energy let totalGain = 0; let currentStat = statTotal; let currentHappy = happy; for (let i = 0; i < numTrains; i++) { const coreComponent = (0.00019106 * currentStat) + (0.00226263 * currentHappy) + 0.55; const baseGain = (gymDots * 4) * coreComponent; const trainGain = (baseGain * bonusMultiplier / 147.24) * energyPerTrain; totalGain += trainGain; currentStat += trainGain; currentHappy = Math.max(0, currentHappy - (energyPerTrain * 0.5)); } // Allowable error const errorMarginSingle = singleGain * 0.00233; const errorMarginTotal = totalGain * 0.00419; // Calculate daily energy with boosters let dailyEnergy = 480; // Natural energy (20 per hour * 24 hours) let boosterEnergy = 0; let boosterCostPerDay = 0; let cooldownHours = 0; let maxBoostersPerDay = 0; if (energyBooster === "xanax") { boosterEnergy = 250; boosterCostPerDay = boosterCount * 880000; cooldownHours = 7; maxBoostersPerDay = Math.floor(24 / cooldownHours); // 3 Xanax per day } else if (energyBooster === "energyCan") { boosterEnergy = 20; boosterCostPerDay = boosterCount * 1166667; cooldownHours = 0.5; // 30 minutes maxBoostersPerDay = 48; // Max 48 cans in 24 hours } else if (energyBooster === "fhc") { boosterEnergy = 150; boosterCostPerDay = boosterCount * 12500000; cooldownHours = 24; maxBoostersPerDay = 1; // 1 FHC per day } else if (energyBooster === "refill") { boosterEnergy = 150; boosterCostPerDay = boosterCount * 1725000; cooldownHours = 0; // No cooldown maxBoostersPerDay = 999; // Arbitrary high limit } boosterCount = Math.min(boosterCount, maxBoostersPerDay); dailyEnergy += boosterCount * boosterEnergy; // Add happiness booster costs if (ecstasy === "yes") { boosterCostPerDay += 100000; // 1 Ecstasy per day } boosterCostPerDay += eroticDVDs * 2500000; // Cost of Erotic DVDs if (steroids > 0) { boosterCostPerDay += 1000000; // 1 Steroids per day } // Calculate daily gains const dailyTrains = Math.floor(dailyEnergy / energyPerTrain); let dailyGain = 0; currentStat = statTotal + totalGain; // Start from after initial train currentHappy = happy; // Reset daily with Ecstasy for (let i = 0; i < dailyTrains; i++) { const coreComponent = (0.00019106 * currentStat) + (0.00226263 * currentHappy) + 0.55; const baseGain = (gymDots * 4) * coreComponent; const trainGain = (baseGain * bonusMultiplier / 147.24) * energyPerTrain; dailyGain += trainGain; currentStat += trainGain; currentHappy = Math.max(0, currentHappy - (energyPerTrain * 0.5)); } // Calculate days to reach goal let daysToGoal = 0; let totalBoosterCost = 0; const maxIterations = 10000; // Prevent infinite loops let iterationCount = 0; while (currentStat < statGoal && iterationCount < maxIterations) { currentStat += dailyGain; daysToGoal++; totalBoosterCost += boosterCostPerDay; iterationCount++; } // Update display document.getElementById("energyPerTrain").textContent = energyPerTrain; document.getElementById("numTrains").textContent = numTrains; document.getElementById("bonusMultiplier").textContent = bonusMultiplier.toFixed(4); document.getElementById("singleGain").textContent = `${singleGain.toFixed(2)} (Min: ${(singleGain - errorMarginSingle).toFixed(2)}, Max: ${(singleGain + errorMarginSingle).toFixed(2)})`; document.getElementById("totalGain").textContent = `${totalGain.toFixed(2)} (Min: ${(totalGain - errorMarginTotal).toFixed(2)}, Max: ${(totalGain + errorMarginTotal).toFixed(2)})`; document.getElementById("errorMargin").textContent = `Single: ±${errorMarginSingle.toFixed(2)}, Total: ±${errorMarginTotal.toFixed(2)}`; document.getElementById("dailyEnergy").textContent = dailyEnergy; document.getElementById("boosterCost").textContent = `$${boosterCostPerDay.toLocaleString()}`; document.getElementById("daysToGoal").textContent = iterationCount >= maxIterations ? "Goal unreachable" : daysToGoal; document.getElementById("totalBoosterCost").textContent = iterationCount >= maxIterations ? "N/A" : `$${totalBoosterCost.toLocaleString()}`; // Show copy button document.getElementById("copyButton").style.display = "inline-block"; } catch (error) { console.error("Calculation error:", error); alert("An error occurred during calculation: " + error.message); // Display default values to ensure output shows document.getElementById("energyPerTrain").textContent = "-"; document.getElementById("numTrains").textContent = "-"; document.getElementById("bonusMultiplier").textContent = "-"; document.getElementById("singleGain").textContent = "-"; document.getElementById("totalGain").textContent = "-"; document.getElementById("errorMargin").textContent = "-"; document.getElementById("dailyEnergy").textContent = "-"; document.getElementById("boosterCost").textContent = "-"; document.getElementById("daysToGoal").textContent = "-"; document.getElementById("totalBoosterCost").textContent = "-"; } } // Copy results to clipboard function copyResults() { try { const singleGain = document.getElementById("singleGain").textContent; const totalGain = document.getElementById("totalGain").textContent; const errorMargin = document.getElementById("errorMargin").textContent; const dailyEnergy = document.getElementById("dailyEnergy").textContent; const boosterCost = document.getElementById("boosterCost").textContent; const daysToGoal = document.getElementById("daysToGoal").textContent; const totalBoosterCost = document.getElementById("totalBoosterCost").textContent; const textToCopy = `Predicted Gains (Single Train): ${singleGain}\n` + `Predicted Gains (Total Initial): ${totalGain}\n` + `Allowable Error (+/-): ${errorMargin}\n` + `Total Energy Per Day: ${dailyEnergy}\n` + `Daily Booster Cost: ${boosterCost}\n` + `Days to Reach Goal: ${daysToGoal}\n` + `Total Booster Cost to Goal: ${totalBoosterCost}`; navigator.clipboard.writeText(textToCopy).then(() => { alert("Results copied to clipboard!"); }).catch(err => { console.error("Failed to copy: ", err); alert("Failed to copy results. Please copy manually."); }); } catch (error) { console.error("Copy error:", error); alert("An error occurred while copying results: " + error.message); } } // Add event listeners const calculateButton = menu.querySelector("#calculateButton"); calculateButton.addEventListener("click", calculateGains); const copyButton = menu.querySelector("#copyButton"); copyButton.addEventListener("click", copyResults); // Changelog Pop-up Logic const currentVersion = "2.53"; const lastSeenVersion = GM_getValue("lastSeenVersion", "0.0"); if (lastSeenVersion !== currentVersion) { const changelogOverlay = document.createElement("div"); changelogOverlay.className = "changelog-overlay"; changelogOverlay.innerHTML = ` <div class="changelog-box"> <h4>TornPDA - Gym Gains Calculator v${currentVersion}</h4> <ul> <li>Added persistence for user entries on page close/refresh using GM_setValue/GM_getValue.</li> <li>Previous: Improved styling of dropdown headers to match Torn's theme.</li> <li>Previous: Moved Bonuses & Boosters menu to the bottom of the main UI.</li> <li>Previous: Fixed TypeError by changing const to let for boosterCount.</li> <li>Previous: Ensured bonuses are in a nested collapsible menu.</li> <li>Previous: Added error handling to ensure output displays.</li> <li>Previous: Added energy and happiness booster dropdowns.</li> <li>Previous: Added desired stat goal input.</li> <li>Previous: Calculated total time to goal with cooldowns.</li> <li>Previous: Added booster cost calculations.</li> </ul> <button id="closeChangelog">Close</button> </div> `; document.body.appendChild(changelogOverlay); const closeButton = changelogOverlay.querySelector("#closeChangelog"); closeButton.addEventListener("click", () => { GM_setValue("lastSeenVersion", currentVersion); changelogOverlay.remove(); }); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址