// ==UserScript==
// @name Torn Bank Investment Calculator with Enhanced Features
// @namespace http://tampermonkey.net/
// @version 3.6
// @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*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const CURRENT_VERSION = '3.6'; // 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.6';
const CHANGELOG = `
<strong>Changelog:</strong>
<ul>
<li><strong>Version 3.6:</strong> Moved UI to the top of the webpage.</li>
<li><strong>Version 3.5:</strong> Optimized for TornPDA update system.</li>
</ul>
`;
function formatCurrency(value) {
if (value >= 1000000000) return `${(value / 1000000000).toFixed(1)}b`;
if (value >= 1000000) return `${(value / 1000000).toFixed(1)}m`;
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 aprValues = Array.from(rateElements).map(el => {
const match = el.textContent.trim().match(/(\d+\.\d+)%/);
return match ? parseFloat(match[1]) : null;
}).filter(v => v !== null);
if (aprValues.length >= 5) {
const meritMultiplier = 1 + (merits * 0.05);
const baseRates = aprValues.map(apr => (apr / 52 / meritMultiplier) * 100);
investmentOptions.forEach((opt, i) => opt.baseRate = baseRates[i] || opt.baseRate);
investmentOptions.forEach(opt => opt.label = `${opt.period} Days (${opt.baseRate.toFixed(2)}% base)`);
console.log('Torn Bank Calc: Updated rates:', investmentOptions);
} else {
console.warn('Torn Bank Calc: Insufficient APR 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 createCalculatorUI() {
console.log('Torn Bank Calc: Attempting to create UI...');
// Prioritize document.body to ensure top placement, with fallback containers
let targetContainer = document.body; // Start with body for absolute top placement
const fallbackContainers = [
document.querySelector('#mainContainer .content-wrapper'),
document.querySelector('.content-wrapper'),
document.querySelector('#bankBlock'),
document.querySelector('.content')
];
// Use the first valid fallback if body insertion fails
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. Aborting UI creation.');
return;
}
if (document.getElementById('torn-bank-calc')) {
console.log('Torn Bank Calc: UI already exists, skipping creation.');
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';
// Load saved preferences
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>
`;
// Insert the calculator at the top of the target container (prioritizing body)
targetContainer.prepend(calcDiv);
// Add version number to bottom left corner
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.');
// Show changelog notification if version is new
showChangelogNotification();
// Collapse by default
const details = document.getElementById('calcDetails');
if (details) {
details.open = false;
}
// Save preferences when changed
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);
});
}
// Initial fetch with saved merits
fetchDynamicBaseRates(parseInt(savedMerits));
}
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) {
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;
periods++;
if (periods > 10000) return { periods: Infinity, days: Infinity, profit: 0 };
}
return { periods, days: periods * period, profit: amount - principal };
}
function calculateProfitAndProjection() {
try {
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;
// Update base rates based on current merits
fetchDynamicBaseRates(merits);
// Update comparison table with shortest path
const comparisonTableBody = document.getElementById('comparisonTableBody');
if (comparisonTableBody) {
updateComparisonTable(principal, target, merits, hasStockBonus);
} else {
throw new Error('Comparison table body not found.');
}
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'}`;
}
}
}
function updateComparisonTable(principal, target, merits, hasStockBonus) {
const tableBody = document.getElementById('comparisonTableBody');
if (!tableBody) {
console.error('Comparison table body not found.');
return;
}
let shortestPath = null;
let shortestDays = Infinity;
// Evaluate each period for both reinvest and no-reinvest options
investmentOptions.forEach(opt => {
const meritMultiplier = 1 + (merits * 0.05);
let effectiveRate = opt.baseRate * meritMultiplier;
if (hasStockBonus) {
effectiveRate *= 1.10;
}
const weeks = opt.period / 7;
const ratePerPeriod = effectiveRate * weeks;
// Calculate time to target without reinvestment
const noReinvest = calculateTimeToTargetNoReinvest(principal, target, ratePerPeriod, opt.period);
if (noReinvest.days < shortestDays) {
shortestDays = noReinvest.days;
shortestPath = {
period: opt.label,
method: 'No Reinvest',
days: noReinvest.days,
profit: noReinvest.profit
};
}
// Calculate time to target with reinvestment
const reinvest = calculateTimeToTargetWithReinvest(principal, target, ratePerPeriod, opt.period);
if (reinvest.days < shortestDays) {
shortestDays = reinvest.days;
shortestPath = {
period: opt.label,
method: 'Reinvest',
days: reinvest.days,
profit: reinvest.profit
};
}
});
// Display the shortest path
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.');
}
}
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();
})();