// ==UserScript==
// @name Torn Bank Investment Calculator with Enhanced Features
// @namespace http://tampermonkey.net/
// @version 4.3
// @description Calculates Torn bank investment profit and time to goal using spreadsheet formulas (FV and NPER). Single Investment and Goal Calculations work independently based on non-zero inputs. Includes Donor Packs support, merit/TCB stock bonuses, and custom interest rates.
// @author Jvmie
// @match https://www.torn.com/bank.php*
// @license MIT
// @grant none
// @require https://code.jquery.com/jquery-3.6.0.min.js
// ==/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 OTHER DEALINGS IN THE
* SOFTWARE.
*/
/* Changelog History (for reference, not displayed in popup):
* Version 4.3 (2025-03-16): - Removed Shortest Path to Goal output section. - Consolidated interest rate input for Goal Calculation to a single field. - Updated description and changelog to reflect changes.
*/
(function() {
'use strict';
const VERSION = '4.3';
const MAX_INVESTMENT = 2e9; // Default max investment limit ($2 billion)
const DONOR_PACK_COST = 22350000; // Cost of one Donor Pack
const CHANGELOG = `
<strong>Changelog:</strong>
<ul>
<li><strong>Version 4.3 (2025-03-16):</strong>
- Removed Shortest Path to Goal output section.
- Consolidated interest rate input for Goal Calculation to a single field.
- Updated description and changelog to reflect changes.
</li>
</ul>
`;
function formatCurrency(value) {
if (value >= 1e9) return `${(value / 1e9).toFixed(1)}b`;
if (value >= 1e6) return `${(value / 1e6).toFixed(1)}m`;
if (value >= 1e3) return `${(value / 1e3).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('b')) return parseFloat(value.replace('b', '')) * 1e9;
if (value.endsWith('m')) return parseFloat(value.replace('m', '')) * 1e6;
if (value.endsWith('k')) return parseFloat(value.replace('k', '')) * 1e3;
return parseFloat(value) || 0;
}
function formatDays(days) {
if (days === Infinity || isNaN(days)) return 'N/A';
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();
}
// Calculate effective weekly rate with merits and TCB stock
function calculateEffectiveWeeklyRate(baseWeeklyRate, merits, hasStockBonus) {
const meritBoost = merits * 0.005; // 0.5% per merit (max 5% at 10 merits)
const stockBoost = hasStockBonus ? 0.10 : 0; // 10% with TCB stock
const effectiveRate = (baseWeeklyRate + meritBoost) * (1 + stockBoost);
return effectiveRate;
}
// Implement Excel FV function
function calculateFV(rate, nper, pmt, pv) {
if (rate === 0) {
return -(pv + pmt * nper);
}
const term = Math.pow(1 + rate, nper);
return -(pv * term + pmt * (term - 1) / rate);
}
// Implement Excel NPER function
function calculateNPER(rate, pmt, pv, fv) {
if (rate === 0) {
return -(fv + pv) / pmt;
}
const numerator = Math.log((fv * rate - pmt) / (pv * rate - pmt));
const denominator = Math.log(1 + rate);
return numerator / denominator;
}
// Calculate total return, profit, and ROI for a single period
function calculateInvestmentReturn(principal, weeklyRate, donorPacks, weeks, merits, hasStockBonus) {
const effectiveRate = calculateEffectiveWeeklyRate(weeklyRate, merits, hasStockBonus);
const pmt = -donorPacks * DONOR_PACK_COST; // Negative because it's an outgoing payment
const pv = -principal; // Negative because it's an outgoing investment
const totalReturn = calculateFV(effectiveRate, weeks, pmt, pv);
const profit = totalReturn - principal - (donorPacks * DONOR_PACK_COST);
const roi = principal !== 0 ? ((totalReturn - principal) / principal) * 100 : 0;
return { totalReturn, profit, roi };
}
// Calculate weeks to reach goal
function calculateWeeksToGoal(principal, target, weeklyRate, donorPacksPerWeek, merits, hasStockBonus) {
const effectiveRate = calculateEffectiveWeeklyRate(weeklyRate, merits, hasStockBonus);
const pmt = -donorPacksPerWeek * DONOR_PACK_COST; // Negative because it's an outgoing payment
const pv = -principal; // Negative because it's an outgoing investment
const fv = target; // Positive because it's the goal
if (effectiveRate === 0 && pmt === 0) return Infinity; // Avoid division by zero
const weeks = calculateNPER(effectiveRate, pmt, pv, fv);
return Math.max(0, Math.ceil(weeks)); // Round up to ensure goal is set
}
// Show changelog pop-up on version change
function showChangelogPopup() {
const lastSeenVersion = localStorage.getItem('tornBankCalcVersion');
if (lastSeenVersion === VERSION) return;
const popup = document.createElement('div');
popup.id = 'torn-bank-calc-changelog';
popup.style.position = 'fixed';
popup.style.top = '50%';
popup.style.left = '50%';
popup.style.transform = 'translate(-50%, -50%)';
popup.style.backgroundColor = '#1c2526';
popup.style.color = '#d0d0d0';
popup.style.padding = '20px';
popup.style.border = '1px solid #2a3439';
popup.style.borderRadius = '5px';
popup.style.zIndex = '10000';
popup.style.maxWidth = '500px';
popup.style.maxHeight = '80vh';
popup.style.overflowY = 'auto';
popup.style.fontFamily = 'Arial, sans-serif';
popup.style.fontSize = '14px';
popup.innerHTML = `
<h3 style="color: #fff; margin-bottom: 15px; text-align: center;">Torn Bank Calc Update</h3>
${CHANGELOG}
<div style="text-align: center; margin-top: 20px;">
<button id="closeChangelog" style="padding: 8px 16px; background: #28a745; color: #fff; border: none; border-radius: 3px; cursor: pointer;">Close</button>
</div>
`;
document.body.appendChild(popup);
const closeButton = document.getElementById('closeChangelog');
if (closeButton) {
closeButton.addEventListener('click', () => {
popup.remove();
localStorage.setItem('tornBankCalcVersion', VERSION);
});
}
}
function createCalculatorUI() {
console.log('Torn Bank Calc: Creating UI...');
if (document.getElementById('torn-bank-calc')) return;
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 savedOilRigBonus = localStorage.getItem('tornBankCalcOilRigBonus') === 'true';
const savedDonorPacks = localStorage.getItem('tornBankCalcDonorPacks') || '';
const savedDonorPacksPerWeek = localStorage.getItem('tornBankCalcDonorPacksPerWeek') || '';
const savedInterestRateSingle = localStorage.getItem('tornBankCalcInterestRateSingle') || 1.04;
const savedInterestRateGoal = localStorage.getItem('tornBankCalcInterestRateGoal') || 1.04;
const meritOptionsHTML = Array.from({ length: 11 }, (_, i) =>
`<option value="${i}" ${i === parseInt(savedMerits) ? 'selected' : ''}>${i} Merits (+${(i * 0.5).toFixed(1)}%)</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;">
<h4 style="color: #fff; margin-bottom: 10px;">Single Investment Calculation</h4>
<label style="display: block; margin-bottom: 5px; color: #d0d0d0;">Starting Investment ($):</label>
<input type="text" id="principalSingle" placeholder="Enter amount (e.g., 500k)" value="" 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;">Investment Length (weeks):</label>
<select id="investmentLength" style="width: 100%; padding: 5px; background: #2a3439; color: #fff; border: 1px solid #3e4a50; border-radius: 3px; margin-bottom: 10px;">
<option value="1">1 Week</option>
<option value="2">2 Weeks</option>
<option value="4">1 Month (4 weeks)</option>
<option value="8">2 Months (8 weeks)</option>
<option value="12">3 Months (12 weeks)</option>
</select>
<label style="display: block; margin-bottom: 5px; color: #d0d0d0;">Interest Rate (% per period):</label>
<input type="number" id="singleInterestRate" step="0.01" min="0" value="${savedInterestRateSingle}" 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;">Donor Packs (total):</label>
<input type="number" id="donorPacksSingle" min="0" value="${savedDonorPacks}" style="width: 100%; padding: 5px; background: #2a3439; color: #fff; border: 1px solid #3e4a50; border-radius: 3px; margin-bottom: 10px;">
<h4 style="color: #fff; margin-bottom: 10px;">Goal Calculation</h4>
<label style="display: block; margin-bottom: 5px; color: #d0d0d0;">Starting Investment ($):</label>
<input type="text" id="principalGoal" placeholder="Enter amount (e.g., 500k)" value="" 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;">Goal Amount ($):</label>
<input type="text" id="targetAmount" placeholder="Enter target (e.g., 525k)" value="" 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;">Investment Length (weeks):</label>
<select id="goalInvestmentLength" style="width: 100%; padding: 5px; background: #2a3439; color: #fff; border: 1px solid #3e4a50; border-radius: 3px; margin-bottom: 10px;">
<option value="1">1 Week</option>
<option value="2">2 Weeks</option>
<option value="4">1 Month (4 weeks)</option>
<option value="8">2 Months (8 weeks)</option>
<option value="12">3 Months (12 weeks)</option>
</select>
<label style="display: block; margin-bottom: 5px; color: #d0d0d0;">Interest Rate (% per period):</label>
<input type="number" id="goalInterestRate" step="0.01" min="0" value="${savedInterestRateGoal}" 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;">Donor Packs (per week):</label>
<input type="number" id="donorPacksPerWeek" min="0" value="${savedDonorPacksPerWeek}" 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>
<label style="display: block; margin-bottom: 10px; color: #d0d0d0;">
<input type="checkbox" id="oilRigBonus" style="vertical-align: middle; margin-right: 5px;" ${savedOilRigBonus ? 'checked' : ''}> In 10* Oil Rig (+$1b)
</label>
<button id="calculateBtn" style="width: 100%; padding: 8px; background: #28a745; color: #fff; border: none; border-radius: 3px; cursor: pointer;">Calculate</button>
<div id="resultSingle" style="margin-top: 15px;">
<h4 style="color: #fff; margin-bottom: 10px;">Single Investment Results:</h4>
<p style="color: #fff;">Total Return: <span id="totalReturn">N/A</span></p>
<p style="color: #fff;">Profit: <span id="profitSingle">N/A</span></p>
<p style="color: #fff;">ROI %: <span id="roiSingle">N/A</span></p>
</div>
<div id="resultGoal" style="margin-top: 15px;">
<h4 style="color: #fff; margin-bottom: 10px;">Goal Calculation Results:</h4>
<p style="color: #fff;">Total Return: <span id="goalTotalReturn">N/A</span></p>
<p style="color: #fff;">Profit: <span id="goalProfit">N/A</span></p>
<p style="color: #fff;">ROI %: <span id="goalRoi">N/A</span></p>
</div>
</div>
</details>
`;
document.body.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.');
const details = document.getElementById('calcDetails');
if (details) details.open = false;
const meritSelect = document.getElementById('meritSelect');
const stockBonus = document.getElementById('stockBonus');
const oilRigBonus = document.getElementById('oilRigBonus');
const donorPacksSingle = document.getElementById('donorPacksSingle');
const donorPacksPerWeek = document.getElementById('donorPacksPerWeek');
const singleInterestRate = document.getElementById('singleInterestRate');
const goalInterestRate = document.getElementById('goalInterestRate');
if (meritSelect) {
meritSelect.addEventListener('change', () => {
localStorage.setItem('tornBankCalcMerits', meritSelect.value);
});
}
if (stockBonus) {
stockBonus.addEventListener('change', () => {
localStorage.setItem('tornBankCalcStockBonus', stockBonus.checked);
});
}
if (oilRigBonus) {
oilRigBonus.addEventListener('change', () => {
localStorage.setItem('tornBankCalcOilRigBonus', oilRigBonus.checked);
});
}
if (donorPacksSingle) {
donorPacksSingle.addEventListener('change', () => {
localStorage.setItem('tornBankCalcDonorPacks', donorPacksSingle.value);
});
}
if (donorPacksPerWeek) {
donorPacksPerWeek.addEventListener('change', () => {
localStorage.setItem('tornBankCalcDonorPacksPerWeek', donorPacksPerWeek.value);
});
}
if (singleInterestRate) {
singleInterestRate.addEventListener('change', () => {
localStorage.setItem('tornBankCalcInterestRateSingle', singleInterestRate.value);
});
}
if (goalInterestRate) {
goalInterestRate.addEventListener('change', () => {
localStorage.setItem('tornBankCalcInterestRateGoal', goalInterestRate.value);
});
}
}
function calculateProfitAndProjection() {
// Get inputs for Single Investment Calculation
const principalSingleInput = document.getElementById('principalSingle').value.trim();
const principalSingle = parseInput(principalSingleInput);
const investmentLength = parseInt(document.getElementById('investmentLength').value);
const singleInterestRate = parseFloat(document.getElementById('singleInterestRate').value) / 100;
const donorPacksSingle = parseInt(document.getElementById('donorPacksSingle').value) || 0;
// Get inputs for Goal Calculation
const principalGoalInput = document.getElementById('principalGoal').value.trim();
const principalGoal = parseInput(principalGoalInput);
const targetInput = document.getElementById('targetAmount').value.trim();
const target = parseInput(targetInput);
const goalInvestmentLength = parseInt(document.getElementById('goalInvestmentLength').value);
const goalInterestRate = parseFloat(document.getElementById('goalInterestRate').value) / 100;
const donorPacksPerWeek = parseInt(document.getElementById('donorPacksPerWeek').value) || 0;
// Common inputs
const meritSelect = document.getElementById('meritSelect');
const merits = meritSelect ? parseInt(meritSelect.value) : 0;
const stockBonus = document.getElementById('stockBonus');
const hasStockBonus = stockBonus ? stockBonus.checked : false;
const oilRigBonus = document.getElementById('oilRigBonus').checked;
// Determine which sections to calculate
const calculateSingle = principalSingle > 0;
const calculateGoal = principalGoal > 0 && target > principalGoal;
if (!calculateSingle && !calculateGoal) {
alert('Please enter a non-zero Starting Investment in at least one section.');
return;
}
// Single Investment Calculation
if (calculateSingle) {
if (principalSingle < 0) {
alert('Starting investment (Single Investment) cannot be negative.');
return;
}
if (singleInterestRate < 0) {
alert('Interest rate (Single Investment) cannot be negative.');
return;
}
if (donorPacksSingle < 0) {
alert('Donor Packs (Single Investment) cannot be negative.');
return;
}
const weeklyRate = singleInterestRate / investmentLength; // Convert per-period rate to weekly rate
const investmentReturn = calculateInvestmentReturn(principalSingle, weeklyRate, donorPacksSingle, investmentLength, merits, hasStockBonus);
document.getElementById('totalReturn').textContent = formatCurrency(investmentReturn.totalReturn);
document.getElementById('profitSingle').textContent = formatCurrency(investmentReturn.profit);
document.getElementById('roiSingle').textContent = isFinite(investmentReturn.roi) ? `${investmentReturn.roi.toFixed(2)}%` : 'N/A';
} else {
// Reset Single Investment Results if not calculated
document.getElementById('totalReturn').textContent = 'N/A';
document.getElementById('profitSingle').textContent = 'N/A';
document.getElementById('roiSingle').textContent = 'N/A';
}
// Goal Calculation
if (calculateGoal) {
if (principalGoal < 0) {
alert('Starting investment (Goal Calculation) cannot be negative.');
return;
}
if (target <= principalGoal) {
alert('Goal amount must be greater than starting investment.');
return;
}
if (goalInterestRate < 0) {
alert('Interest rate (Goal Calculation) cannot be negative.');
return;
}
if (donorPacksPerWeek < 0) {
alert('Donor Packs per week (Goal Calculation) cannot be negative.');
return;
}
const weeklyRate = goalInterestRate / goalInvestmentLength; // Convert per-period rate to weekly rate
const totalWeeks = calculateWeeksToGoal(principalGoal, target, weeklyRate, donorPacksPerWeek, merits, hasStockBonus);
if (totalWeeks === Infinity || isNaN(totalWeeks)) {
document.getElementById('goalTotalReturn').textContent = 'N/A';
document.getElementById('goalProfit').textContent = 'N/A';
document.getElementById('goalRoi').textContent = 'N/A';
console.error('Torn Bank Calc: Unable to calculate weeks to goal.');
return;
}
const investmentReturn = calculateInvestmentReturn(principalGoal, weeklyRate, donorPacksPerWeek * totalWeeks, totalWeeks, merits, hasStockBonus);
// Cap the total return at the goal amount to prevent over-calculation
const cappedTotalReturn = Math.min(investmentReturn.totalReturn, target);
const profit = cappedTotalReturn - principalGoal - (donorPacksPerWeek * totalWeeks * DONOR_PACK_COST);
const roi = principalGoal !== 0 ? ((cappedTotalReturn - principalGoal) / principalGoal) * 100 : 0;
document.getElementById('goalTotalReturn').textContent = formatCurrency(cappedTotalReturn);
document.getElementById('goalProfit').textContent = formatCurrency(profit);
document.getElementById('goalRoi').textContent = isFinite(roi) ? `${roi.toFixed(2)}%` : 'N/A';
} else {
// Reset Goal Calculation Results if not calculated
document.getElementById('goalTotalReturn').textContent = 'N/A';
document.getElementById('goalProfit').textContent = 'N/A';
document.getElementById('goalRoi').textContent = 'N/A';
}
}
function init() {
console.log('Torn Bank Calc: Initializing script...');
showChangelogPopup();
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.');
}
}
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);
}
}
if (window.location.href.match(/https:\/\/www\.torn\.com\/bank\.php/)) {
waitForPageLoad();
}
})();