Energy To Burn

Calculate optimal energy to burn before Xanax cooldown is ready

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Energy To Burn
// @namespace    TornScripts.EnergyBurn
// @license      GPL-3.0
// @version      1.0.1
// @description  Calculate optimal energy to burn before Xanax cooldown is ready
// @author       ButtChew [3840391]
// @icon         https://www.torn.com/favicon.ico
// @homepageURL  https://greasyfork.org/en/scripts/555959-energy-to-burn
// @match        https://www.torn.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @connect      api.torn.com
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // ========== SETTINGS ==========
    const SETTINGS = {
        apiKey: GM_getValue('tornApiKey', ''),
        gymEnergyCost: GM_getValue('gymEnergyCost', 10)
    };

    // Register Tampermonkey menu commands
    GM_registerMenuCommand('⚙️ Energy Burn Settings', openSettingsModal);

    // ========== UTILITY FUNCTIONS ==========
    function formatTime(seconds) {
        if (seconds <= 0) return 'Ready!';
        const hours = Math.floor(seconds / 3600);
        const minutes = Math.floor((seconds % 3600) / 60);
        const secs = seconds % 60;

        if (hours > 0) {
            return `${hours}h ${minutes}m`;
        } else if (minutes > 0) {
            return `${minutes}m ${secs}s`;
        } else {
            return `${secs}s`;
        }
    }

    function formatNumber(num) {
        return num.toLocaleString('en-US');
    }

    // ========== API FUNCTIONS ==========
    function fetchTornAPI(endpoint, callback) {
        const apiKey = GM_getValue('tornApiKey', '');

        if (!apiKey) {
            showError('API Key not set. Please configure in Tampermonkey menu: Energy Burn Settings');
            return;
        }

        const url = `https://api.torn.com/v2/user/${endpoint}?key=${apiKey}`;

        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            onload: function(response) {
                if (response.status === 200) {
                    try {
                        const data = JSON.parse(response.responseText);
                        if (data.error) {
                            showError(`API Error: ${data.error.error}`);
                        } else {
                            callback(data);
                        }
                    } catch (e) {
                        showError('Failed to parse API response');
                    }
                } else {
                    showError(`API request failed: ${response.status}`);
                }
            },
            onerror: function() {
                showError('Network error while fetching API data');
            }
        });
    }

    // ========== CALCULATION LOGIC ==========
    function calculateEnergyBurn(energyData, cooldownData) {
        const currentEnergy = energyData.bars.energy.current;
        const maxEnergy = energyData.bars.energy.maximum;
        const energyIncrement = energyData.bars.energy.increment;
        const energyInterval = energyData.bars.energy.interval;
        const energyFullTime = energyData.bars.energy.full_time;
        const energyTickTime = energyData.bars.energy.tick_time;

        const xanaxCooldown = cooldownData.cooldowns.drug;
        const gymEnergyCost = GM_getValue('gymEnergyCost', 10);

        // If Xanax is ready or will be ready before energy is full
        if (xanaxCooldown <= energyFullTime) {
            return {
                currentEnergy: currentEnergy,
                maxEnergy: maxEnergy,
                energyFullTime: energyFullTime,
                xanaxCooldown: xanaxCooldown,
                energyToBurn: 0,
                actionsToDo: 0,
                actualBurn: 0,
                gymEnergyCost: gymEnergyCost,
                message: 'No energy burn needed! Xanax will be ready before or when energy is full.'
            };
        }

        // Calculate optimal energy to burn
        const optimalEnergyToBurn = (xanaxCooldown * energyIncrement / energyInterval) + currentEnergy - maxEnergy;

        // Round down to gym increment (never over-burn)
        const actionsToDo = Math.floor(optimalEnergyToBurn / gymEnergyCost);
        const actualBurn = actionsToDo * gymEnergyCost;

        // Check if user has enough energy to burn right now
        const canBurnNow = currentEnergy >= actualBurn;

        // Calculate time until user has enough energy to burn
        let timeUntilCanBurn = 0;
        if (!canBurnNow && actualBurn > 0) {
            const energyNeededToBurn = actualBurn - currentEnergy;
            const ticksNeededToBurn = Math.ceil(energyNeededToBurn / energyIncrement);
            // First tick happens at tick_time, remaining ticks at interval
            timeUntilCanBurn = energyTickTime + ((ticksNeededToBurn - 1) * energyInterval);
        }

        // Calculate new full time after burning
        // If can't burn now, assume we burn at 0 (after waiting to regenerate the needed amount)
        const newEnergy = canBurnNow ? currentEnergy - actualBurn : 0;
        const energyNeeded = maxEnergy - newEnergy;
        const ticksNeeded = Math.ceil(energyNeeded / energyIncrement);
        const newFullTime = canBurnNow ? ticksNeeded * energyInterval : (timeUntilCanBurn + ticksNeeded * energyInterval);

        return {
            currentEnergy: currentEnergy,
            maxEnergy: maxEnergy,
            energyFullTime: energyFullTime,
            xanaxCooldown: xanaxCooldown,
            energyToBurn: optimalEnergyToBurn,
            actionsToDo: actionsToDo,
            actualBurn: actualBurn,
            newEnergy: newEnergy,
            newFullTime: newFullTime,
            gymEnergyCost: gymEnergyCost,
            timeDifference: Math.abs(newFullTime - xanaxCooldown),
            canBurnNow: canBurnNow,
            timeUntilCanBurn: timeUntilCanBurn,
            message: null
        };
    }

    // ========== UI FUNCTIONS ==========
    function addSidebarLink() {
        // Wait for sidebar to load
        const checkSidebar = setInterval(() => {
            const areasSection = document.querySelector('.areas___ElnyB .toggle-content___BJ9Q9');
            if (areasSection && !document.getElementById('nav-energy_burn')) {
                clearInterval(checkSidebar);

                // Create the energy burn link
                const energyBurnDiv = document.createElement('div');
                energyBurnDiv.className = 'area-desktop___bpqAS';
                energyBurnDiv.id = 'nav-energy_burn';
                energyBurnDiv.innerHTML = `
                    <div class="area-row___iBD8N">
                        <a href="#" class="desktopLink___SG2RU" id="energy-burn-link">
                            <span class="svgIconWrap___AMIqR">
                                <span class="defaultIcon___iiNis mobile___paLva">
                                    <svg xmlns="http://www.w3.org/2000/svg" stroke="transparent" stroke-width="0" width="16" height="20" viewBox="0 0 16 20">
                                        <path d="M8.5,0L0,10h6l-2,10l10-12H8L8.5,0z" fill="url(#sidebar_svg_gradient_regular_desktop)"/>
                                    </svg>
                                </span>
                            </span>
                            <span class="linkName___FoKha">Energy Burn</span>
                        </a>
                    </div>
                `;

                // Insert after Gym (or at end of areas)
                const gymNav = document.getElementById('nav-gym');
                if (gymNav && gymNav.nextSibling) {
                    areasSection.insertBefore(energyBurnDiv, gymNav.nextSibling);
                } else {
                    areasSection.appendChild(energyBurnDiv);
                }

                // Add click handler
                document.getElementById('energy-burn-link').addEventListener('click', (e) => {
                    e.preventDefault();
                    openEnergyBurnModal();
                });
            }
        }, 500);

        // Clear interval after 10 seconds if sidebar not found
        setTimeout(() => clearInterval(checkSidebar), 10000);
    }

    function openEnergyBurnModal() {
        // Check if API key is set
        if (!GM_getValue('tornApiKey', '')) {
            if (confirm('API Key not configured. Would you like to set it up now?')) {
                openSettingsModal();
            }
            return;
        }

        // Create modal overlay
        if (document.getElementById('energy-burn-modal')) {
            document.getElementById('energy-burn-modal').remove();
        }

        const modal = document.createElement('div');
        modal.id = 'energy-burn-modal';
        modal.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.8);
            z-index: 99999;
            display: flex;
            align-items: center;
            justify-content: center;
        `;

        modal.innerHTML = `
            <div style="
                background: #2e2e2e;
                border-radius: 10px;
                width: 500px;
                max-width: 90%;
                box-shadow: 0 4px 20px rgba(0,0,0,0.5);
            ">
                <div style="
                    background: linear-gradient(to bottom, #1a1a1a, #2a2a2a);
                    padding: 15px 20px;
                    border-radius: 10px 10px 0 0;
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                ">
                    <h2 style="margin: 0; color: #fff; font-size: 18px;">⚡ Energy To Burn</h2>
                    <button id="close-energy-modal" style="
                        background: none;
                        border: none;
                        color: #aaa;
                        font-size: 24px;
                        cursor: pointer;
                        padding: 0;
                        width: 30px;
                        height: 30px;
                    ">×</button>
                </div>
                <div style="
                    padding: 20px;
                    color: #ccc;
                ">
                    <div id="energy-burn-content" style="text-align: center; padding: 20px;">
                        <p style="color: #aaa;">Loading data...</p>
                        <div style="
                            border: 3px solid #f3f3f3;
                            border-radius: 50%;
                            border-top: 3px solid #3498db;
                            width: 40px;
                            height: 40px;
                            animation: spin 1s linear infinite;
                            margin: 20px auto;
                        "></div>
                    </div>
                    <div style="margin-top: 15px; text-align: center;">
                        <button id="refresh-energy-data" class="torn-btn" style="
                            background: linear-gradient(to bottom, #799427, #a3c248);
                            border: none;
                            color: white;
                            padding: 10px 20px;
                            border-radius: 5px;
                            cursor: pointer;
                            font-size: 14px;
                            margin-right: 10px;
                            display: inline-flex;
                            align-items: center;
                            justify-content: center;
                            vertical-align: middle;
                        ">🔄 Refresh</button>
                        <button id="open-settings-btn" class="torn-btn" style="
                            background: linear-gradient(to bottom, #555, #777);
                            border: none;
                            color: white;
                            padding: 10px 20px;
                            border-radius: 5px;
                            cursor: pointer;
                            font-size: 14px;
                            display: inline-flex;
                            align-items: center;
                            justify-content: center;
                            vertical-align: middle;
                        ">⚙️ Settings</button>
                    </div>
                </div>
            </div>
        `;

        // Add spinner animation
        const style = document.createElement('style');
        style.textContent = `
            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }
        `;
        document.head.appendChild(style);

        document.body.appendChild(modal);

        // Event listeners
        document.getElementById('close-energy-modal').addEventListener('click', () => {
            modal.remove();
        });

        document.getElementById('refresh-energy-data').addEventListener('click', () => {
            loadEnergyData();
        });

        document.getElementById('open-settings-btn').addEventListener('click', () => {
            openSettingsModal();
        });

        modal.addEventListener('click', (e) => {
            if (e.target === modal) {
                modal.remove();
            }
        });

        // Load data
        loadEnergyData();
    }

    function loadEnergyData() {
        const contentDiv = document.getElementById('energy-burn-content');
        contentDiv.innerHTML = `
            <p style="color: #aaa;">Fetching energy data...</p>
            <div style="
                border: 3px solid #f3f3f3;
                border-radius: 50%;
                border-top: 3px solid #3498db;
                width: 40px;
                height: 40px;
                animation: spin 1s linear infinite;
                margin: 20px auto;
            "></div>
        `;

        // Fetch both APIs
        let energyData = null;
        let cooldownData = null;

        fetchTornAPI('bars', (data) => {
            energyData = data;
            checkAndDisplay();
        });

        fetchTornAPI('cooldowns', (data) => {
            cooldownData = data;
            checkAndDisplay();
        });

        function checkAndDisplay() {
            if (energyData && cooldownData) {
                displayResults(energyData, cooldownData);
            }
        }
    }

    function displayResults(energyData, cooldownData) {
        const result = calculateEnergyBurn(energyData, cooldownData);
        const contentDiv = document.getElementById('energy-burn-content');

        if (result.message) {
            // No burn needed
            contentDiv.innerHTML = `
                <div style="text-align: center; padding: 10px;">
                    <div style="font-size: 48px; margin-bottom: 10px;">✅</div>
                    <p style="font-size: 16px; color: #90EE90; font-weight: bold;">${result.message}</p>
                    <hr style="border: 1px solid #444; margin: 20px 0;">
                    <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; text-align: left;">
                        <div>
                            <p style="color: #aaa; margin: 5px 0;">Current Energy:</p>
                            <p style="color: #fff; font-size: 18px; font-weight: bold; margin: 5px 0;">${result.currentEnergy} / ${result.maxEnergy}</p>
                        </div>
                        <div>
                            <p style="color: #aaa; margin: 5px 0;">Energy Full In:</p>
                            <p style="color: #fff; font-size: 18px; font-weight: bold; margin: 5px 0;">${formatTime(result.energyFullTime)}</p>
                        </div>
                        <div>
                            <p style="color: #aaa; margin: 5px 0;">Xanax Ready In:</p>
                            <p style="color: #fff; font-size: 18px; font-weight: bold; margin: 5px 0;">${formatTime(result.xanaxCooldown)}</p>
                        </div>
                        <div>
                            <p style="color: #aaa; margin: 5px 0;">Gym Energy Cost:</p>
                            <p style="color: #fff; font-size: 18px; font-weight: bold; margin: 5px 0;">${result.gymEnergyCost}</p>
                        </div>
                    </div>
                </div>
            `;
        } else {
            // Burn needed
            const earlyLate = result.newFullTime > result.xanaxCooldown ?
                `<span style="color: #FFB84D;">${formatTime(result.timeDifference)} after Xanax ready</span>` :
                `<span style="color: #90EE90;">${formatTime(result.timeDifference)} before Xanax ready</span>`;

            contentDiv.innerHTML = `
                <div style="padding: 10px;">
                    <div style="background: rgba(255, 107, 62, 0.2); border-left: 4px solid #EF6B3E; padding: 15px; margin-bottom: 20px; border-radius: 5px;">
                        <div style="font-size: 14px; color: #aaa; margin-bottom: 5px;">RECOMMENDED ACTION</div>
                        <div style="font-size: 24px; color: #EF6B3E; font-weight: bold;">
                            Burn ${formatNumber(result.actualBurn)} Energy
                        </div>
                        <div style="font-size: 14px; color: #ccc; margin-top: 5px;">
                            = ${result.actionsToDo} Gym Train${result.actionsToDo !== 1 ? 's' : ''} @ ${result.gymEnergyCost} energy each
                        </div>
                    </div>

                    <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;">
                        <div style="background: rgba(255,255,255,0.05); padding: 10px; border-radius: 5px;">
                            <p style="color: #aaa; font-size: 12px; margin: 0 0 5px 0;">CURRENT ENERGY</p>
                            <p style="color: #fff; font-size: 20px; font-weight: bold; margin: 0;">${result.currentEnergy} / ${result.maxEnergy}</p>
                        </div>
                        <div style="background: rgba(255,255,255,0.05); padding: 10px; border-radius: 5px;">
                            <p style="color: #aaa; font-size: 12px; margin: 0 0 5px 0;">ENERGY FULL IN</p>
                            <p style="color: #FFB84D; font-size: 20px; font-weight: bold; margin: 0;">${formatTime(result.energyFullTime)}</p>
                        </div>
                        <div style="background: rgba(255,255,255,0.05); padding: 10px; border-radius: 5px;">
                            <p style="color: #aaa; font-size: 12px; margin: 0 0 5px 0;">XANAX READY IN</p>
                            <p style="color: #90EE90; font-size: 20px; font-weight: bold; margin: 0;">${formatTime(result.xanaxCooldown)}</p>
                        </div>
                        <div style="background: rgba(255,255,255,0.05); padding: 10px; border-radius: 5px;">
                            <p style="color: #aaa; font-size: 12px; margin: 0 0 5px 0;">CAN BURN IN</p>
                            <p style="color: ${result.canBurnNow ? '#90EE90' : '#FFB84D'}; font-size: 20px; font-weight: bold; margin: 0;">
                                ${result.canBurnNow ? 'READY!' : formatTime(result.timeUntilCanBurn)}
                            </p>
                        </div>
                    </div>

                    <hr style="border: 1px solid #444; margin: 15px 0;">

                    <div style="background: rgba(144, 238, 144, 0.1); padding: 15px; border-radius: 5px; border-left: 4px solid #90EE90;">
                        <p style="color: #aaa; font-size: 12px; margin: 0 0 10px 0;">AFTER BURNING ${formatNumber(result.actualBurn)} ENERGY:</p>
                        <p style="color: #fff; margin: 5px 0;">Energy Full In: <strong>${formatTime(result.newFullTime)}</strong></p>
                        <p style="color: #ccc; margin: 5px 0; font-size: 13px;">Energy reaches full ${earlyLate}</p>
                    </div>

                    ${result.actualBurn < result.energyToBurn ? `
                        <div style="background: rgba(255, 184, 77, 0.1); padding: 10px; margin-top: 10px; border-radius: 5px; font-size: 12px; color: #FFB84D;">
                            ℹ️ Note: Optimal burn is ${result.energyToBurn.toFixed(1)} energy, but we round down to ${result.actualBurn} to ensure energy is full when Xanax is ready.
                        </div>
                    ` : ''}
                </div>
            `;
        }
    }

    function showError(message) {
        const contentDiv = document.getElementById('energy-burn-content');
        if (contentDiv) {
            contentDiv.innerHTML = `
                <div style="text-align: center; padding: 20px;">
                    <div style="font-size: 48px; color: #EF6B3E; margin-bottom: 10px;">⚠️</div>
                    <p style="color: #EF6B3E; font-size: 16px; font-weight: bold;">Error</p>
                    <p style="color: #ccc; margin-top: 10px;">${message}</p>
                </div>
            `;
        } else {
            alert('Energy Burn Error: ' + message);
        }
    }

    function openSettingsModal() {
        const currentApiKey = GM_getValue('tornApiKey', '');
        const currentGymCost = GM_getValue('gymEnergyCost', 10);

        const settingsModal = document.createElement('div');
        settingsModal.id = 'energy-settings-modal';
        settingsModal.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.8);
            z-index: 100000;
            display: flex;
            align-items: center;
            justify-content: center;
        `;

        settingsModal.innerHTML = `
            <div style="
                background: #2e2e2e;
                border-radius: 10px;
                width: 450px;
                max-width: 90%;
                box-shadow: 0 4px 20px rgba(0,0,0,0.5);
            ">
                <div style="
                    background: linear-gradient(to bottom, #1a1a1a, #2a2a2a);
                    padding: 15px 20px;
                    border-radius: 10px 10px 0 0;
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                ">
                    <h2 style="margin: 0; color: #fff; font-size: 18px;">⚙️ Energy Burn Settings</h2>
                    <button id="close-settings-modal" style="
                        background: none;
                        border: none;
                        color: #aaa;
                        font-size: 24px;
                        cursor: pointer;
                        padding: 0;
                        width: 30px;
                        height: 30px;
                    ">×</button>
                </div>
                <div style="padding: 20px; color: #ccc;">
                    <div style="margin-bottom: 20px;">
                        <label style="display: block; margin-bottom: 5px; color: #aaa; font-size: 14px;">
                            Torn API Key:
                        </label>
                        <input type="text" id="api-key-input" value="${currentApiKey}" placeholder="Enter your Torn API key" style="
                            width: 100%;
                            padding: 10px;
                            background: #1a1a1a;
                            border: 1px solid #444;
                            border-radius: 5px;
                            color: #fff;
                            font-size: 14px;
                            box-sizing: border-box;
                        ">
                        <p style="font-size: 12px; color: #888; margin-top: 5px;">
                            Requires "Minimal" API key or higher.
                        </p>
                    </div>

                    <div style="margin-bottom: 20px;">
                        <label style="display: block; margin-bottom: 5px; color: #aaa; font-size: 14px;">
                            Gym Energy Cost per Train:
                        </label>
                        <input type="number" id="gym-energy-input" value="${currentGymCost}" min="1" max="100" style="
                            width: 100%;
                            padding: 10px;
                            background: #1a1a1a;
                            border: 1px solid #444;
                            border-radius: 5px;
                            color: #fff;
                            font-size: 14px;
                            box-sizing: border-box;
                        ">
                        <p style="font-size: 12px; color: #888; margin-top: 5px;">
                            Common values: 10 (basic gyms), 20, 50 (advanced gyms)
                        </p>
                    </div>

                    <div style="text-align: right;">
                        <button id="cancel-settings" style="
                            background: linear-gradient(to bottom, #555, #777);
                            border: none;
                            color: white;
                            padding: 10px 20px;
                            border-radius: 5px;
                            cursor: pointer;
                            font-size: 14px;
                            margin-right: 10px;
                            display: inline-flex;
                            align-items: center;
                            justify-content: center;
                            vertical-align: middle;
                        ">Cancel</button>
                        <button id="save-settings" style="
                            background: linear-gradient(to bottom, #799427, #a3c248);
                            border: none;
                            color: white;
                            padding: 10px 20px;
                            border-radius: 5px;
                            cursor: pointer;
                            font-size: 14px;
                            display: inline-flex;
                            align-items: center;
                            justify-content: center;
                            vertical-align: middle;
                        ">Save Settings</button>
                    </div>
                </div>
            </div>
        `;

        document.body.appendChild(settingsModal);

        // Event listeners
        document.getElementById('close-settings-modal').addEventListener('click', () => {
            settingsModal.remove();
        });

        document.getElementById('cancel-settings').addEventListener('click', () => {
            settingsModal.remove();
        });

        document.getElementById('save-settings').addEventListener('click', () => {
            const apiKey = document.getElementById('api-key-input').value.trim();
            const gymCost = parseInt(document.getElementById('gym-energy-input').value);

            if (!apiKey) {
                alert('Please enter a valid API key');
                return;
            }

            if (gymCost < 1 || gymCost > 100) {
                alert('Gym energy cost must be between 1 and 100');
                return;
            }

            GM_setValue('tornApiKey', apiKey);
            GM_setValue('gymEnergyCost', gymCost);

            alert('Settings saved successfully!');
            settingsModal.remove();

            // Refresh data if main modal is open
            if (document.getElementById('energy-burn-modal')) {
                loadEnergyData();
            }
        });

        settingsModal.addEventListener('click', (e) => {
            if (e.target === settingsModal) {
                settingsModal.remove();
            }
        });
    }

    // ========== INITIALIZATION ==========
    function init() {
        // Wait for page to fully load
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', addSidebarLink);
        } else {
            addSidebarLink();
        }
    }

    init();
})();