Torn Bank Investment Calculator with Enhanced Features

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.

目前為 2025-03-16 提交的版本,檢視 最新版本

// ==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();
    }
})();