// ==UserScript==
// @name Torn Bank Investment Calculator with Enhanced Features
// @namespace http://tampermonkey.net/
// @version 3.12
// @description Calculates Torn bank investment profit with merits, TCB stock, and displays the shortest path to target amount with profit and time.
// @author Jvmie
// @match https://www.torn.com/bank.php*
// @license MIT
// @grant none
// ==/UserScript==
/*
* MIT License
*
* Copyright (c) 2025 ItzJamiie
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/* Changelog History (for reference, not displayed in popup):
* Version 3.11: Fixed "Comparison table body not found" error by improving UI creation and error handling. Added hospital state detection; calculator is disabled when in hospital. Fixed formatting bug in formatCurrency for million-range values.
* Version 3.10: Added @license MIT tag to comply with Greasy Fork requirements.
* Version 3.9: Added MIT License to the script as the sole author.
* Version 3.8: Confirmed changelog popup behavior: shows only current version's updates, appears once in the middle of the page.
* Version 3.7: Updated changelog popup to show only current version's updates. Version increments now applied for each script change.
* Version 3.6: Moved UI to the top of the webpage.
* Version 3.5: Optimized for TornPDA update system.
*/
(function() {
'use strict';
const CURRENT_VERSION = '3.12'; // Must match @version
const SCRIPT_URL = 'https://raw.githubusercontent.com/ItzJamiie/torn-bank-calc/main/torn-bank-calc.user.js';
// Function to check for updates (for compatibility with TornPDA)
function checkForScriptUpdate() {
fetch(SCRIPT_URL)
.then(response => response.text())
.then(text => {
const versionMatch = text.match(/@version\s+([\d.]+)/);
if (versionMatch && versionMatch[1] && versionMatch[1] !== CURRENT_VERSION) {
console.log(`Torn Bank Calc: New version ${versionMatch[1]} detected. Notify TornPDA to update.`);
// TornPDA may handle the update prompt; this log helps debugging
}
})
.catch(err => console.error('Torn Bank Calc: Update check failed:', err));
}
setInterval(checkForScriptUpdate, 300000); // Check every 5 minutes
let investmentOptions = [
{ period: 7, baseRate: 0.6889, label: '7 Days (0.69% base)' },
{ period: 14, baseRate: 0.800, label: '14 Days (0.80% base)' },
{ period: 30, baseRate: 0.833, label: '30 Days (0.83% base)' },
{ period: 60, baseRate: 0.953, label: '60 Days (0.95% base)' },
{ period: 90, baseRate: 0.953, label: '90 Days (0.95% base)' }
];
const MAX_INVESTMENT = 2000000000;
const VERSION = '3.12';
const CHANGELOG = `
<strong>Changelog (Version 3.12):</strong>
<ul>
<li>Adjusted fetchDynamicBaseRates to correctly interpret weekly rates from Torn bank page.</li>
<li>Improved comparison logic to reflect weekly reinvestment vs. longer-term investments.</li>
</ul>
`;
function formatCurrency(value) {
if (value >= 1000000000) return `${(value / 1000000000).toFixed(1)}b`;
if (value >= 1000000) return `${(value / 1000000).toFixed(1)}m`; // Corrected for million range
if (value >= 1000) return `${(value / 1000).toFixed(0)}k`;
return `$${value.toLocaleString('en-US', { minimumFractionDigits: 2 })}`;
}
function parseInput(value) {
value = value.trim().toLowerCase().replace(/[^0-9.kmb]/g, '');
if (value.endsWith('k')) return parseFloat(value.replace('k', '')) * 1000;
if (value.endsWith('m')) return parseFloat(value.replace('m', '')) * 1000000;
if (value.endsWith('b')) return parseFloat(value.replace('b', '')) * 1000000000;
return parseFloat(value) || 0;
}
function formatDays(days) {
if (days === Infinity) return 'N/A (Cap Reached)';
if (days === 0) return '0d';
const years = Math.floor(days / 365);
const months = Math.floor((days % 365) / 30);
const daysRemain = days % 30;
let result = '';
if (years > 0) result += `${years}y `;
if (months > 0 || years > 0) result += `${months}m `;
result += `${daysRemain}d`;
return result.trim();
}
function fetchDynamicBaseRates(merits) {
try {
const rateElements = document.querySelectorAll('.bar-label, .apr-value, [class*="apr"]');
if (!rateElements.length) {
console.warn('Torn Bank Calc: Using default rates.');
return;
}
const weeklyRates = Array.from(rateElements).map(el => {
const match = el.textContent.trim().match(/([\d.]+)%/); // Match any percentage
return match ? parseFloat(match[1]) : null;
}).filter(v => v !== null);
if (weeklyRates.length >= 5) {
const meritMultiplier = 1 + (merits * 0.05);
const stockMultiplier = 1.10; // Assuming TCB stock bonus
const totalMultiplier = meritMultiplier * stockMultiplier;
// Assume weeklyRates are effective rates after bonuses; derive base rates
const baseRates = weeklyRates.map(rate => rate / totalMultiplier);
investmentOptions.forEach((opt, i) => {
opt.baseRate = baseRates[i] || opt.baseRate;
opt.label = `${opt.period} Days (${(opt.baseRate * 100).toFixed(2)}% base)`;
});
console.log('Torn Bank Calc: Updated rates (base):', investmentOptions);
} else {
console.warn('Torn Bank Calc: Insufficient rate data.');
}
} catch (e) {
console.error('Torn Bank Calc: Rate fetch error:', e);
}
}
function showChangelogNotification() {
const lastSeen = localStorage.getItem('tornBankCalcLastSeenVersion');
if (lastSeen === VERSION) return;
const div = document.createElement('div');
div.id = 'torn-bank-calc-changelog';
div.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#1c2526;border:1px solid #3e4a50;border-radius:5px;padding:10px;max-width:300px;color:#fff;font-family:Arial,sans-serif;font-size:14px;z-index:10000;box-shadow:0 2px 5px rgba(0,0,0,0.5)';
div.innerHTML = `<div style="margin-bottom:10px;">${CHANGELOG}</div><button id="closeChangelogBtn" style="display:block;margin:0 auto;padding:5px 10px;background:#28a745;color:#fff;border:none;border-radius:3px;cursor:pointer">Close</button>`;
document.body.appendChild(div);
document.getElementById('closeChangelogBtn').addEventListener('click', () => {
div.remove();
localStorage.setItem('tornBankCalcLastSeenVersion', VERSION);
});
}
function isInHospital() {
const hospitalMessage = document.querySelector('div.msg.right-round[aria-label="Information message"]');
if (hospitalMessage && hospitalMessage.textContent.includes("This area is unavailable while you're in hospital")) {
return true;
}
return false;
}
function createCalculatorUI() {
console.log('Torn Bank Calc: Attempting to create UI...');
if (document.getElementById('torn-bank-calc')) {
console.log('Torn Bank Calc: UI already exists, skipping creation.');
return;
}
if (isInHospital()) {
console.log('Torn Bank Calc: User is in hospital, creating restricted UI.');
const calcDiv = document.createElement('div');
calcDiv.id = 'torn-bank-calc';
calcDiv.style.marginTop = '20px';
calcDiv.style.fontFamily = 'Arial, sans-serif';
calcDiv.style.fontSize = '14px';
calcDiv.style.maxWidth = '400px';
calcDiv.style.overflowX = 'hidden';
calcDiv.innerHTML = `
<details id="calcDetails" style="margin-bottom: 10px; border: 1px solid #2a3439; border-radius: 5px;">
<summary style="cursor: pointer; padding: 10px; background: #28a745; border-radius: 3px; color: #fff; text-align: center; font-weight: bold;">Investment Calculator</summary>
<div style="padding: 15px; background: #1c2526; border-radius: 0 0 3px 3px;">
<p style="color: #ffffff; text-align: center;">Calculator unavailable while in hospital.</p>
</div>
</details>
`;
document.body.prepend(calcDiv);
console.log('Torn Bank Calc: Restricted UI (hospital mode) added to the top of the page.');
return;
}
let targetContainer = document.body;
const fallbackContainers = [
document.querySelector('#mainContainer .content-wrapper'),
document.querySelector('.content-wrapper'),
document.querySelector('#bankBlock'),
document.querySelector('.content')
];
if (!targetContainer || targetContainer.children.length === 0) {
for (let container of fallbackContainers) {
if (container) {
targetContainer = container;
break;
}
}
}
if (!targetContainer) {
console.error('Torn Bank Calc: Could not find target container. Forcing UI creation on document.body.');
targetContainer = document.body;
}
const calcDiv = document.createElement('div');
calcDiv.id = 'torn-bank-calc';
calcDiv.style.marginTop = '20px';
calcDiv.style.fontFamily = 'Arial, sans-serif';
calcDiv.style.fontSize = '14px';
calcDiv.style.maxWidth = '400px';
calcDiv.style.overflowX = 'hidden';
const savedMerits = localStorage.getItem('tornBankCalcMerits') || '0';
const savedStockBonus = localStorage.getItem('tornBankCalcStockBonus') === 'true';
const meritOptionsHTML = Array.from({ length: 11 }, (_, i) =>
`<option value="${i}" ${i === parseInt(savedMerits) ? 'selected' : ''}>${i} Merits (+${i * 5}%)</option>`
).join('');
calcDiv.innerHTML = `
<details id="calcDetails" style="margin-bottom: 10px; border: 1px solid #2a3439; border-radius: 5px;">
<summary style="cursor: pointer; padding: 10px; background: #28a745; border-radius: 3px; color: #fff; text-align: center; font-weight: bold;">Investment Calculator</summary>
<div style="padding: 15px; background: #1c2526; border-radius: 0 0 3px 3px;">
<label style="display: block; margin-bottom: 5px; color: #d0d0d0;">Principal ($):</label>
<input type="text" id="principal" placeholder="Enter amount (e.g., 2000m)" style="width: 100%; padding: 5px; background: #2a3439; color: #fff; border: 1px solid #3e4a50; border-radius: 3px; margin-bottom: 10px;">
<label style="display: block; margin-bottom: 5px; color: #d0d0d0;">Target Amount ($):</label>
<input type="text" id="targetAmount" placeholder="Enter target (e.g., 3000m)" style="width: 100%; padding: 5px; background: #2a3439; color: #fff; border: 1px solid #3e4a50; border-radius: 3px; margin-bottom: 10px;">
<label style="display: block; margin-bottom: 5px; color: #d0d0d0;">Bank Merits:</label>
<select id="meritSelect" style="width: 100%; padding: 5px; background: #2a3439; color: #fff; border: 1px solid #3e4a50; border-radius: 3px; margin-bottom: 10px;">
${meritOptionsHTML}
</select>
<label style="display: block; margin-bottom: 10px; color: #d0d0d0;">
<input type="checkbox" id="stockBonus" style="vertical-align: middle; margin-right: 5px;" ${savedStockBonus ? 'checked' : ''}> Own TCB Stock (+10%)
</label>
<button id="calculateBtn" style="width: 100%; padding: 8px; background: #28a745; color: #fff; border: none; border-radius: 3px; cursor: pointer;">Calculate</button>
<div id="result" style="margin-top: 15px;">
<label style="display: block; margin-top: 10px; color: #fff;">Shortest Path to Target:</label>
<table id="comparisonTable" style="width: 100%; max-width: 400px; border-collapse: collapse; margin-top: 10px; table-layout: fixed;">
<thead>
<tr style="background: #2a3439;">
<th style="padding: 5px; border: 1px solid #3e4a50; color: #ffffff; width: 25%;">Period</th>
<th style="padding: 5px; border: 1px solid #3e4a50; color: #ffffff; width: 25%;">Method</th>
<th style="padding: 5px; border: 1px solid #3e4a50; color: #ffffff; width: 25%;">Time to Target</th>
<th style="padding: 5px; border: 1px solid #3e4a50; color: #ffffff; width: 25%;">Profit</th>
</tr>
</thead>
<tbody id="comparisonTableBody">
<tr><td colspan="4" style="text-align: center; padding: 5px; color: #ffffff;">Enter values and calculate to compare</td></tr>
</tbody>
</table>
</div>
</div>
</details>
`;
targetContainer.prepend(calcDiv);
const versionDiv = document.createElement('div');
versionDiv.id = 'torn-bank-calc-version';
versionDiv.style.position = 'fixed';
versionDiv.style.bottom = '10px';
versionDiv.style.left = '10px';
versionDiv.style.color = '#ffffff';
versionDiv.style.fontSize = '12px';
versionDiv.style.fontFamily = 'Arial, sans-serif';
versionDiv.style.zIndex = '1000';
versionDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
versionDiv.style.padding = '2px 5px';
versionDiv.style.borderRadius = '3px';
versionDiv.innerHTML = `Torn Bank Calc V${VERSION}`;
document.body.appendChild(versionDiv);
console.log('Torn Bank Calc: Calculator UI added to the top of the page.');
showChangelogNotification();
const details = document.getElementById('calcDetails');
if (details) {
details.open = false;
}
const meritSelect = document.getElementById('meritSelect');
const stockBonus = document.getElementById('stockBonus');
if (meritSelect) {
meritSelect.addEventListener('change', () => {
localStorage.setItem('tornBankCalcMerits', meritSelect.value);
fetchDynamicBaseRates(parseInt(meritSelect.value));
});
}
if (stockBonus) {
stockBonus.addEventListener('change', () => {
localStorage.setItem('tornBankCalcStockBonus', stockBonus.checked);
});
}
fetchDynamicBaseRates(parseInt(savedMerits));
}
function isInHospital() {
const hospitalMessage = document.querySelector('div.msg.right-round[aria-label="Information message"]');
if (hospitalMessage && hospitalMessage.textContent.includes("This area is unavailable while you're in hospital")) {
return true;
}
return false;
}
function calculateTimeToTargetNoReinvest(principal, target, rate, period) {
if (principal >= target) return { periods: 0, days: 0, profit: 0 };
const ratePerPeriod = rate / 100;
const profitPerPeriod = principal * ratePerPeriod;
const periodsNeeded = Math.ceil((target - principal) / profitPerPeriod);
return { periods: periodsNeeded, days: periodsNeeded * period, profit: periodsNeeded * profitPerPeriod };
}
function calculateTimeToTargetWithReinvest(principal, target, rate, period, additionalFunds = 0) {
if (principal >= target) return { periods: 0, days: 0, profit: 0 };
const ratePerPeriod = rate / 100;
let amount = principal, periods = 0;
while (amount < target) {
let invest = Math.min(amount, MAX_INVESTMENT);
amount += invest * ratePerPeriod + additionalFunds;
periods++;
if (periods > 10000) return { periods: Infinity, days: Infinity, profit: 0 };
}
return { periods, days: periods * period, profit: amount - principal };
}
function calculateProfitAndProjection() {
try {
if (isInHospital()) {
throw new Error('Calculator unavailable while in hospital.');
}
const principalInput = document.getElementById('principal').value.trim();
let principal = parseInput(principalInput);
if (principal < 1000) {
throw new Error('Principal too low. Enter a valid amount (e.g., 2000m).');
}
const targetInput = document.getElementById('targetAmount').value.trim();
let target = parseInput(targetInput);
if (target <= principal) {
throw new Error('Target amount must be greater than principal.');
}
const meritSelect = document.getElementById('meritSelect');
const merits = meritSelect ? parseInt(meritSelect.value) : 0;
const stockBonus = document.getElementById('stockBonus');
const hasStockBonus = stockBonus ? stockBonus.checked : false;
fetchDynamicBaseRates(merits);
updateComparisonTable(principal, target, merits, hasStockBonus);
console.log('Torn Bank Calc: Principal:', principal, 'Target:', target, 'Merits:', merits, 'Stock Bonus:', hasStockBonus);
} catch (e) {
console.error('Torn Bank Calc Error:', e);
const resultDiv = document.getElementById('result');
if (resultDiv) {
resultDiv.innerHTML = `Error: ${e.message || 'Check inputs'}`;
} else {
console.error('Torn Bank Calc: Result div not found. UI may not have been created properly.');
}
}
}
function updateComparisonTable(principal, target, merits, hasStockBonus) {
const tableBody = document.getElementById('comparisonTableBody');
if (!tableBody) {
console.error('Torn Bank Calc: Comparison table body not found. UI may not have been created properly.');
const resultDiv = document.getElementById('result');
if (resultDiv) {
resultDiv.innerHTML = 'Error: Unable to display results. Please refresh the page and try again.';
}
return;
}
let shortestPath = null;
let shortestDays = Infinity;
const meritMultiplier = 1 + (merits * 0.05);
const stockMultiplier = hasStockBonus ? 1.10 : 1.0;
const totalMultiplier = meritMultiplier * stockMultiplier;
investmentOptions.forEach(opt => {
const effectiveRate = opt.baseRate * totalMultiplier; // Base rate is now the weekly rate before bonuses
// No reinvest option
const noReinvest = calculateTimeToTargetNoReinvest(principal, target, effectiveRate, opt.period);
if (noReinvest.days < shortestDays) {
shortestDays = noReinvest.days;
shortestPath = {
period: opt.label,
method: 'No Reinvest',
days: noReinvest.days,
profit: noReinvest.profit
};
}
// Reinvest option (with potential to add funds weekly)
const reinvest = calculateTimeToTargetWithReinvest(principal, target, effectiveRate, opt.period, 0); // No additional funds for now
if (reinvest.days < shortestDays) {
shortestDays = reinvest.days;
shortestPath = {
period: opt.label,
method: 'Reinvest',
days: reinvest.days,
profit: reinvest.profit
};
}
});
let row = '';
if (shortestPath) {
row = `
<tr>
<td style="padding: 5px; border: 1px solid #3e4a50; color: #ffffff; text-align: center; width: 25%;">${shortestPath.period}</td>
<td style="padding: 5px; border: 1px solid #3e4a50; color: #ffffff; text-align: center; width: 25%;">${shortestPath.method}</td>
<td style="padding: 5px; border: 1px solid #3e4a50; color: #ffffff; text-align: center; width: 25%;">${formatDays(shortestPath.days)}</td>
<td style="padding: 5px; border: 1px solid #3e4a50; color: #ffffff; text-align: center; width: 25%;">${shortestPath.days === Infinity ? 'N/A' : formatCurrency(shortestPath.profit)}</td>
</tr>
`;
} else {
row = `<tr><td colspan="4" style="text-align: center; padding: 5px; color: #ffffff;">No valid path found</td></tr>`;
}
tableBody.innerHTML = row;
}
function init() {
console.log('Torn Bank Calc: Initializing script...');
createCalculatorUI();
const calculateBtn = document.getElementById('calculateBtn');
if (calculateBtn) {
calculateBtn.addEventListener('click', calculateProfitAndProjection);
console.log('Torn Bank Calc: Calculate button event listener added.');
} else {
console.error('Torn Bank Calc: Calculate button not found after UI creation. User may be in hospital or UI creation failed.');
}
}
function waitForPageLoad() {
console.log('Torn Bank Calc: Waiting for page load...');
if (document.readyState === 'complete') {
console.log('Torn Bank Calc: Page already loaded, initializing...');
init();
} else {
window.addEventListener('load', () => {
console.log('Torn Bank Calc: Page load event triggered, initializing...');
init();
});
let attempts = 0;
const maxAttempts = 10;
const interval = setInterval(() => {
attempts++;
console.log(`Torn Bank Calc: Attempt ${attempts} to initialize...`);
if (document.readyState === 'complete') {
clearInterval(interval);
init();
} else if (attempts >= maxAttempts) {
clearInterval(interval);
console.error('Torn Bank Calc: Max attempts reached, forcing initialization...');
init();
}
}, 1000);
}
}
waitForPageLoad();
})();