Zed City Better Bars

Uses API to fetch full recharge times & track XP progress manually, accounting for membership bonuses.

// ==UserScript==
// @name         Zed City Better Bars
// @namespace    http://tampermonkey.net/
// @version      6.2
// @description  Uses API to fetch full recharge times & track XP progress manually, accounting for membership bonuses.
// @author       You
// @license      MIT
// @match        https://www.zed.city/*
// @icon         https://www.s2.favicons?sz=64&domain=zed.city
// @icon         https://www.google.com/s2/favicons?sz=64&domain=zed.city
// @grant        none
// ==/UserScript==

(function () {
    "use strict";

    console.log("📢 Zed City Userscript Loaded!");

    let userData = null;
    let towerData = null;
    let raidCooldownElement = null;
    let boosterCooldownElement = null; // Fix: Define boosterCooldownElement
    let radioTowerElement = null;
    async function fetchUserData() {
        try {
            let response = await fetch("https://api.zed.city/getStats", {
                method: "GET",
                credentials: "include",
            });
            let data = await response.json();

            if (data.error) {
                console.warn(`⚠️ API Error: ${data.error}`);
                return;
            }

            userData = data;
            console.log("📡 User Data Fetched:", userData);
            updateLevelElement();
            updateRaidCooldown();
            updateBoosterCooldown();
        } catch (error) {
            console.error("❌ Error fetching user data:", error);
        }
    }

    function fetchRadioTowerData() {
        console.log("🔄 Fetching Radio Tower timer on page load...");
        fetch("https://api.zed.city/getRadioTower", {
            method: "GET",
            credentials: "include", // Ensures authentication cookies are sent
        })
            .then(response => response.json())
            .then(data => {
                if (data.error) {
                    console.warn(`⚠️ API Error: ${data.error}`);
                    return;
                }

                if (data.expire !== undefined) {
                    towerData = data.expire;
                    logRadioTowerTimer();
                } else {
                    console.warn("⚠️ No Radio Tower timer found in response.");
                }
            })
            .catch(error => console.error("❌ Error fetching Radio Tower data:", error));
    }

    function calculateFullRechargeTime(currentValue, maxValue, regenRate, regenInterval, nextTick) {
        let statRemaining = maxValue - currentValue;
        if (statRemaining <= 0) return { fullTime: "Full", timeUntilFull: "00:00" };

        let firstTickTime = nextTick * 1000; // Convert seconds to milliseconds
        let fullTicksNeeded = Math.ceil(statRemaining / regenRate);
        let additionalTime = (fullTicksNeeded - 1) * regenInterval * 60 * 1000;
        let totalTimeNeeded = firstTickTime + additionalTime;
        let fullRechargeDate = new Date(Date.now() + totalTimeNeeded);

        let totalMinutesRemaining = Math.floor(totalTimeNeeded / 60000);
        let hoursRemaining = Math.floor(totalMinutesRemaining / 60);
        let minutesRemaining = totalMinutesRemaining % 60;
        let timeUntilFull = `${String(hoursRemaining).padStart(2, "0")}:${String(minutesRemaining).padStart(2, "0")}`;

        return {
            fullTime: fullRechargeDate.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false }),
            timeUntilFull,
        };
    }

    async function addStatDisplay(statKey, label, baseRegenRate, regenInterval, barClass) {
        try {
            let statBar = await waitForElement(`.q-linear-progress.${barClass}`);
            if (!statBar) return;

            console.log(`✅ Found ${label} progress bar`);

            let infoElement = document.createElement("div");
            Object.assign(infoElement.style, {
                padding: "5px",
                marginTop: "5px",
                background: "rgba(0, 0, 0, 0.7)",
                color: "white",
                borderRadius: "5px",
                fontSize: "12px",
                textAlign: "center",
                cursor: "pointer"
            });

            statBar.parentNode.insertBefore(infoElement, statBar.nextSibling);

            let lastDisplayedText = "";

            setInterval(() => {
                if (userData) {
                    let isMember = userData?.membership ? true : false;
                    let adjustedRegenRate = baseRegenRate;
                    let adjustedMaxValue = userData.skills[`max_${statKey}`] || 0;
                    let currentValue = userData[statKey] || 0;
                    let nextTick = userData[`${statKey}_regen`] || 0;

                    if (statKey === "energy" && isMember) {
                        adjustedRegenRate = 5;
                        adjustedMaxValue = 150;
                    }

                    let { fullTime, timeUntilFull } = calculateFullRechargeTime(
                        currentValue, adjustedMaxValue, adjustedRegenRate, regenInterval, nextTick
                    );

                    let newDisplayText = `${label} ${fullTime} (${timeUntilFull})`;

                    if (newDisplayText !== lastDisplayedText) {
                        infoElement.textContent = newDisplayText;
                        lastDisplayedText = newDisplayText;
                    }
                }
            }, 1000);
        } catch (error) {
            console.error(`❌ Error modifying ${label} bar:`, error);
        }
    }

async function addCooldownDisplays() {
    try {
        let numberRow = await waitForElement(".currency-stats");
        if (!numberRow) return;

        // Raid Cooldown Element
        raidCooldownElement = document.createElement("div");
        Object.assign(raidCooldownElement.style, {
            color: "white",
            borderRadius: "5px",
            fontSize: "12px",
            textAlign: "center",
            cursor: "pointer"
        });
        raidCooldownElement.innerText = "⚔️ Raid: Loading...";
        numberRow.prepend(raidCooldownElement);

        // Booster Cooldown Element
        boosterCooldownElement = document.createElement("div");
        Object.assign(boosterCooldownElement.style, {
            color: "white",
            borderRadius: "5px",
            fontSize: "12px",
            textAlign: "center",
            cursor: "pointer"
        });
        boosterCooldownElement.innerText = "Booster: Loading...";
        numberRow.appendChild(boosterCooldownElement);

        // Radio Tower Display Element (Appended at the end)
        radioTowerElement = document.createElement("div");
        Object.assign(radioTowerElement.style, {
            color: "white",
            borderRadius: "5px",
            fontSize: "12px",
            textAlign: "center",
            cursor: "pointer"
        });
        radioTowerElement.innerText = "📡 Radio Tower: Coming Soon";
        numberRow.appendChild(radioTowerElement);

    } catch (error) {
        console.error("❌ Error adding cooldown displays:", error);
    }
}

    function updateRaidCooldown() {
        if (userData && userData.raid_cooldown !== undefined && raidCooldownElement) {
            let remainingSeconds = userData.raid_cooldown;
            let minutes = Math.floor(remainingSeconds / 60);
            let seconds = remainingSeconds % 60;
            raidCooldownElement.innerText = `⚔️ Raid: ${minutes}m ${seconds}s`;
        }
    }

    function updateBoosterCooldown() {
        if (userData && userData.booster_cooldown !== undefined && boosterCooldownElement) {
            let remainingSeconds = userData.booster_cooldown;
            let minutes = Math.floor(remainingSeconds / 60);
            let seconds = remainingSeconds % 60;
            boosterCooldownElement.innerText = `Booster: ${minutes}m ${seconds}s`;
        }
        else{
            boosterCooldownElement.innerText = `Booster: None`;
        }
    }

    /**
     * Logs the Radio Tower timer
     */
    function logRadioTowerTimer() {
        if(towerData != undefined & radioTowerElement){
        let timeLeft = towerData;
        let hours = Math.floor(timeLeft / 3600);
        let minutes = Math.floor((timeLeft % 3600) / 60);
        let seconds = timeLeft % 60;
        console.log(`📡 Radio Tower Timer: ${timeLeft} seconds`);
        console.log(`📡 Radio Tower Timer (formatted): ${hours}h ${minutes}m ${seconds}s`);
        radioTowerElement.innerText = `📡${hours}h ${minutes}m ${seconds}s`;
        }
    }


    async function updateLevelElement() {
        await waitForElement(".stat-level");

        let elem = document.querySelector(".stat-level");
        if (!elem || !userData) return;

        let levelText = elem.querySelector(".row.items-center.no-wrap div:last-child");
        if (!levelText) return;

        let currentXP = Math.round(userData.experience - userData.xp_start);
        let xpToNextLevel = Math.round(userData.xp_end - userData.xp_start);

        levelText.innerText = `${userData.rank} (${currentXP}/${xpToNextLevel})`;
    }

    async function waitForElement(selector, timeout = 10000) {
        return new Promise((resolve, reject) => {
            const observer = new MutationObserver(() => {
                let element = document.querySelector(selector);
                if (element) {
                    observer.disconnect();
                    resolve(element);
                }
            });

            observer.observe(document.body, { childList: true, subtree: true });

            setTimeout(() => {
                observer.disconnect();
                reject(`❌ Timeout: Element '${selector}' not found`);
            }, timeout);
        });
    }


    // Run the script
    addStatDisplay("energy", "Energy", 5, 10, "energy");
    addStatDisplay("rad", "Radiation", 1, 5, "rad");
    addStatDisplay("morale", "Morale", 5, 5, "morale");
    addStatDisplay("life", "Life", 10, 15, "life");
    addCooldownDisplays();
    fetchRadioTowerData();
    setInterval(fetchUserData, 5000);
    setInterval(fetchRadioTowerData, 5000);
})();