ZedHelper

Misc helper tools for Zed City

Stan na 18-09-2025. Zobacz najnowsza wersja.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         ZedHelper
// @description  Misc helper tools for Zed City
// @version      0.4.17
// @namespace    kvassh.zedhelper
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=zed.city
// @homepage     https://greasyfork.org/en/scripts/527868-zedhelper
// @author       Kvassh
// @match        https://www.zed.city/*
// @run-at       document-end
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        window.onurlchange
// @connect      api.zed.city
// ==/UserScript==

/** 
 * ZedHelper
 * 
 * Features:
 * - Displays market value for items in inventory
 * - Calculates your inventory networth based on current market values
 * - Extra nav menu with some useful shortcuts (togglable in settings)
 * - Autopopulates gym train input fields to use maximum energy
 * - Autopopulates input field for junk shop with 360 item buy qty
 * - Show value of trades at Radio Tower
 * - Show timer for various features (Raid, Junk Store limit)
 *
 * If you have any questions, feel free to reach out to Kvassh [12853] in Zed City
 * 
 * Changelog:
 * - 0.4.17: Add Max Rad btn on scavenge page
 *           Make icons/links smaller on the timer bar
 * - 0.4.16: Try to click MAX button to set input to max in stores
 *           Add more buttons to timer bar (Gym, RadioTower, Scavenge)
 *           Make the timer links go to the page without reloading webbrowser page
 * - 0.4.15: Add bulk scavenge buttons for 5 and 30 scavenges at a time.
 *           Fix bug in Radio Tower trade value calculation.
 * - 0.4.14: Another fix for the duplicate timer bar issue, hopefully 100% fixed now.
 * - 0.4.13: Fix error with duplicate timer bar appearing.
 *           Add link to respective functions for timers also when not ready
 * - 0.4.12: Fix another bug in time parsing for timer bar
 * - 0.4.11: Fix bug in time parsing for timer bar
 * - 0.4.10: Fix correct link for Raid timer shortcut
 * - 0.4.9: Add timer for Raid
 * - 0.4.8: Add timer for Junk Store limit
 * - 0.4.7: Fix container width for mobile in Settings page.
 * - 0.4.6: Add ZH icon to statusbar that points to new Settings page.
 *          Add setting for toggling extra nav menu on or off.
 *          Include cash on hand when calculating networth.
 * - 0.4.5: Avoid duplicate inventory networth elements
 *          Less padding for item values in inventory to fit better on mobile.
 * - 0.4.4: Change homepage and downloadURL to use greasyfork.org + change icon to zed.city favicon.
 * - 0.4.3: Use navigation navigate eventlistener instead to detect page change.
 * - 0.4.2: Try to force window eventlistener for urlchange to work on mobile.
 * - 0.4.1: Show warning if market values has not been cached yet. 
 *          Show warning on Radio Tower if the cached data is old.
 *          Indicate if the trade is good or bad with checkmark on Radio Tower.
 *          Fixed bug on inventory page where it would potentially not update prices if changing to next page in inventorylist.
 * - 0.4: Add value of trades at Radio Tower.
 * - 0.3: Fix bug in gym autopopulate + add new autopopulate in junk store for 360 items.
 * - 0.2: Add feature to autopopulate gym input fields.
 * - 0.1: Initial release.
*/

(function() {
    'use strict';

    // Add CSS for displaying prices (optional, but makes it look nicer)
    GM_addStyle(`
        .market-price {
            color:#999999;
            float:right;
            position:absolute;
            top:18px;
            right:100px;
        }
        .green {
            color: #00cc66;
        }
        .red {
            color: #ff6666;
        }
        .gray {
            color: #888;
        }
        .zedhelper-networth {
            text-align: center;
            margin: 10px auto;
            color: #ccc;
            font-size: 1.6rem;
        }
        .zedhelper-inventory-warning {
            text-align: center;
            margin: 10px auto;
            color: #ccc;
            font-size: 0.8rem;
        }
        .radio-warning {
            text-align: center;
        }
        .zedhelper-timer-bar {
            margin-top:0px;
        }
        .zedhelper-timer-span {
            padding: 0 10px;
        }
        .zedhelper-timer-span a {
            text-decoration:none;
        }
    `);

    const baseApiUrl = 'https://api.zed.city';

    /** Dont modify anything below this line */

    let module = "index";
    let checkForInventoryUpdates = null;








    /** Utils */

    function get(key) {
        return localStorage.getItem(`kvassh.zedhelper.${key}`);
    }
    function set(key, value) {
        localStorage.setItem(`kvassh.zedhelper.${key}`, value);
    }

    function log(msg) {
        const spacer = "          ";
        const ts = new Date();
        console.log("ZedHelper (" + ts.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' } )+ ") " +
            "[" + module + "]" + ((module.length < spacer.length) ? spacer.substring(0, spacer.length - module.length) : "") + ": " +
            (typeof msg === 'object' ? JSON.stringify(msg) : msg));
    }

    function waitForElement(selector) {
        return new Promise(resolve => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }
    
            const observer = new MutationObserver(mutations => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });
    
            // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    }

    function getCodename(itemName) {
        let codename = itemName.toString().toLowerCase().replace(' ', '_').trim().split(/\n/)[0];

        let nametable = {
            "arrows": "ammo_arrows",
            "bows": "ammo_bows",
            "logs": "craft_log",
            "nails": "craft_nails",
            "rope": "craft_rope",
            "scrap": "craft_scrap",
            "wire": "craft_wire",
            "army_helmet": "defense_army_helmet",
            "camo_hat": "defense_camo_hat",
            "camo_vest": "defense_camo_vest",
            "e-cola": "ecola",
            "lighter":"misc_lighter",
            "lockpick":"misc_lockpick",
            "pickaxe":"misc_pickaxe",
            "security_card":"defense_security_card",
            "zed_coin": "points",
            "baseball_bat": "weapon_baseball_bat",
            "bow":"weapon_bow",
            "chainsaw":"weapon_chainsaw",
            "spear":"weapon_spear",
            "switchblade":"weapon_switchblade",
        };

        for (const [key, value] of Object.entries(nametable)) {
            if (codename === key) {
                return value;
            }
        }
        return codename;
    }

    function formatNumber(number) {
        const formatter = new Intl.NumberFormat('nb-NO', {
            maximumFractionDigits: 0,
            

        });
        return formatter.format(number);
    }


















    /** XHR Interceptor */

    const originalXHR = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (...args) {
        this.addEventListener('load', function () {
            const url = this.responseURL;

            // if (url.includes("/getOffers")) {
            //     const item = JSON.parse(this.responseText)[0];
            //     log(`Caching market value for: ${item['name']} (${item['codename']})`);
            //     set(`mv_${item["codename"]}`, JSON.stringify({ "name": item["name"], "marketValue": item["market_price"], "tz": Date.now() }));
            // }

            if (url.endsWith("/getMarket")) {
                const items = JSON.parse(this.responseText).items;
                let itemsCached = 0;
                for (let item of items) {
                    let codename = getCodename(item["name"]);
                    set(`mv_${codename}`, JSON.stringify({ "name": item["name"], "marketValue": item["market_price"], "tz": Date.now() }));
                    itemsCached++;
                }
                set(`mv_lastupdate`, Date.now());
                log(`Cached market value for ${itemsCached} items.`);
            }

            else if (url.endsWith("/loadItems")) {
                const data = JSON.parse(this.responseText);
                const items = data.items;
                let networthVendor = 0;
                let networthMarket = 0;
                for (let item of items) {

                    networthVendor += item.value * item.quantity;

                    const codename = item.codename;
                    if(get(`mv_${codename}`)) {
                        const mv = JSON.parse(get(`mv_${codename}`));
                        networthMarket += mv.marketValue * item.quantity;
                    } else {
                        networthMarket += item.value * item.quantity;
                    }
                }
                set(`mv_networth_vendor`, networthVendor);
                set(`mv_networth_market`, networthMarket);
                log(`cached inventory networth (vendor: ${networthVendor}, market: ${networthMarket})`);
            }

            else if (url.endsWith("/getStats")) {
                const data = JSON.parse(this.responseText);
                set(`energy`, data.energy);
                set(`morale`, data.morale);
                set(`rad`, data.rad);
                set(`refills`, data.refills);
                set(`money`, data.money);
                set(`xpUntilNextRank`, parseInt(data.xp_end-data.experience));

                set(`raidCooldownSecondsLeft`, data.raid_cooldown); 
                set(`raidCooldownTime`, Date.now()); 
            }

            else if (url.endsWith("/getRadioTower")) {
                const data = JSON.parse(this.responseText);
                saveCurrentTradeValues(data);
                set(`radio_lastupdate`, Date.now());
            }

            else if (url.endsWith("/getStore?store_id=junk")) {
                const data = JSON.parse(this.responseText);
                if (data.hasOwnProperty('limits')) {
                    set(`junkStoreLimitSecondsLeft`, data.limits.reset_time); 
                    set(`junkStoreLimitTime`, Date.now()); 
                } else {
                    set(`junkStoreLimitSecondsLeft`, 0); 
                    set(`junkStoreLimitTime`, 0); 
                }
            }

            else if (url.endsWith("/getStore?store_id=zedmart")) {
                const data = JSON.parse(this.responseText);
                if (data.hasOwnProperty('limits')) {
                    set(`zedMartLimitSecondsLeft`, data.limits.reset_time); 
                    set(`zedMartLimitTime`, Date.now()); 
                } else {
                    set(`zedMartLimitSecondsLeft`, 0); 
                    set(`zedMartLimitTime`, 0); 
                }
            }

        });
        originalXHR.apply(this, args);
    };


















    /** Main script */

    log("Starting up ZedHelper!");

    let navigationTimeout = null;

    let urlChangeHandler = async () => {
        if (navigationTimeout === null) {

            const page = location.pathname;

            // Ensure we dont watch for inventory updates after changing subpage
            clearInterval(checkForInventoryUpdates);
            checkForInventoryUpdates = null;

            // Update the timer bar
            addZedHelperIconAndTimerBar();

            if (page.includes("inventory")) {
                module = "inventory";
                // log("Waiting for inventory list...");
                
                waitForElement("#q-app > div > div.q-page-container > main > div > div:nth-child(4) > div > div.grid-cont.no-padding").then(() => {
                    waitForElement(".item-row").then(() => {
                        log("Inventory list loaded! Adding market prices...");
                        addMarketPrices();
                    });
                });

                waitForElement("#q-app > div > div.q-page-container > main > div").then(() => {
                    showNetworth();
                });
            }
            else if (page.includes("market-listings")) {
                module = "market";
                log("Navigated to Market Listings - Watching for element to add new listing...");
                waitForElement("div > div > button.q-btn.q-btn-item.bg-positive").then(() => {
                    log("Detected form for adding new market listing... showing market values for inventory!");
                    addMarketPrices();
                })
            }
            else if (page.includes("stronghold/2375014")) {
                module = "gym";
                log("Navigated to Gym");
                autoPopulateTrainInput();
            }
            else if (page.includes("stronghold/2375016")) {
                module = "crafting";
                log("Navigated to Crafting Bench");
            }
            else if (page.includes("stronghold/2375017")) {
                module = "furnace";
                log("Navigated to Furnace");
            }
            else if (page.includes("stronghold/2375019")) {
                module = "radio";
                log("Navigated to Radio Tower");
                setTimeout(() => {
                    showTradeValues();
                },1000);
            }
            else if (page.includes("/store/")) {
                module = "store";
                log("Setting up auto input for store - click max btn automatically");
                // autoPopulate360Items();
                autoPopulateMaxItems();
            }
            else if (page.includes("/zedhelper")) {
                showSettingsPage();
            }
            else if (/\/scavenge\/\d+$/.test(page)) {
                module = "scavenge";
                log("Navigated to Scavenge");
                addBulkScavengeButtons();
            }
            else {
                module = "unknown";
                log(`Unknown subpage: ${page}`);
            }

            navigationTimeout = setTimeout(() => {
                clearTimeout(navigationTimeout);
                navigationTimeout = null;
            }, 250);
        }
    }

    try {
        navigation.addEventListener('navigate', () => {
            setTimeout(() => {
                urlChangeHandler();
            },100);
        });
    } catch (error) {
        log("FATAL ERROR: Could not add EventListener for navigation navigate: " + JSON.stringify(error));
    }













    /** Add a second nav menu with some useful shortcuts */

    // document.querySelector("#q-app > div > header > div:nth-child(2) > div > div > div").app
    const secondNavBar = document.createElement('div');
    secondNavBar.innerHTML = `
<div>
    <div class="gt-xs bg-grey-3 text-grey-5 text-h6">
        <div class="q-tabs row no-wrap items-center q-tabs--not-scrollable q-tabs--horizontal q-tabs__arrows--inside q-tabs--mobile-with-arrows q-tabs--dense" role="tablist" inside-arrows="">

            <div class="q-tabs__content scroll--mobile row no-wrap items-center self-stretch hide-scrollbar relative-position q-tabs__content--align-center">

                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/stronghold/2375017">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                        <div class="q-tab__label">Furnace</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>
                
                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/stronghold/2375014">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                        <div class="q-tab__label">Gym</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>

                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/scavenge/2">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                    <div class="q-tab__label">Scrapyard</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>

                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/market">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                    <div class="q-tab__label">Market</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>

                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/store/junk">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                    <div class="q-tab__label">Junk Store</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>

                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/zedhelper">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                    <div class="q-tab__label">Settings</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>

            </div>
        </div>
    </div>
    `;  

    if(get('extraNavMenu') && (get('extraNavMenu') === 'true' || get('extraNavMenu') === true)) {
        log("Enabling extra navigation menu");
        waitForElement("#q-app > div > header").then(() => {
            document.querySelector("#q-app > div > header").appendChild(secondNavBar);
        });
    }











    /** Add icon for ZedHelper settings + timer bar */

    function addZedHelperIconAndTimerBar() {

        const zedHelperIcon = document.createElement('div');
        zedHelperIcon.classList = 'zedhelper-icon-bar';
        zedHelperIcon.innerHTML = `
    <div class="row items-center">
        <b><a href="/zedhelper" title="ZedHelper Settings" style="color:dodgerblue;text-decoration:none;font-weight:bold;">ZH</a></b>
    </div>
        `;

        const timerBar = document.createElement('div');
        timerBar.classList = "row q-col-gutter-md justify-center items-center zedhelper-timer-bar";

        let timeDiff = 0;
        let timeLeft = 0;
        let timeLeftFormatted = "";

        let html = `<div class="q-tab__label">`;

        /** GYM */
        // const maxEnergy = 150;
        // const currentEnergy = get('energy') || 0; // Retrieve current energy from storage
        // const energyRegenRate = 5; // Energy regenerated per interval
        // const regenIntervalMinutes = 10; // Interval in minutes
        // const missingEnergy = maxEnergy - currentEnergy;
        // const timeToFullEnergyMinutes = Math.ceil((missingEnergy / energyRegenRate) * regenIntervalMinutes);
        // if (timeToFullEnergyMinutes > 0) {
        //     const hours = Math.floor(timeToFullEnergyMinutes / 60);
        //     const minutes = timeToFullEnergyMinutes % 60;
        //     timeLeftFormatted = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
        // }
        // html += `<span class="zedhelper-timer-span">Gym: <a id="zhOpenGym" style="cursor:pointer;">${timeToFullEnergyMinutes > 0 ? `<span class="red">${timeLeftFormatted}</span>` : `<span class="green">Ready</span>`}</a></span>`;
        // html += `<span class="zedhelper-timer-span"><a id="zhOpenGym" style="cursor:pointer;">⚡</a></span>`;
        // html += `<span class="zedhelper-timer-span"><a id="zhOpenGym" style="cursor:pointer;"><img src="https://www.zed.city/assets/gym-cOgAonBN.png" style="width:20px;position:relative;top:5px;"></a></span>`;
        html += `<span class="zedhelper-timer-span"><a id="zhOpenGym" style="cursor:pointer;">💪</a></span>`;

        /** RADIO TOWER */
        // const last = get("radioTower_last_visited") || 0;
        // const ready = parseInt(last) + (12*60*60*1000);
        // timeLeft = ready - Date.now();
        // const h = Math.floor(timeLeft / 36e5);
        // const m = Math.floor((timeLeft % 36e5) / 6e4);
        // const s = Math.floor((timeLeft % 6e4) / 1000);
        // // return `${h}h ${m}m ${s}s left`;
        // console.log(`last: ${last} - ready: ${ready} - now: ${Date.now()} - timeLeft: ${timeLeft}`);
        // // console.log("timeLeft: " + timeLeft);
        // try {
        //     timeLeftFormatted = new Date(timeLeft * 1000).toISOString().substr(11, 5);
        // } catch (error) {
        //     timeLeftFormatted = "00:00";
        // }
        // html += `<span class="zedhelper-timer-span">Radio&nbsp;Tower: <a id="zhOpenRadioTower" style="cursor:pointer;">${timeLeft > 0 ? `<span class="red">${timeLeftFormatted}</span>` : `<span class="green">Ready</span>`}</a></span>`;
        // html += `<span class="zedhelper-timer-span"><a id="zhOpenRadioTower" style="cursor:pointer;"><img src="https://www.zed.city/assets/radio_tower-DZgBlHS5.png" style="width:20px;position:relative;top:5px;"></a></span>`;
        html += `<span class="zedhelper-timer-span"><a id="zhOpenRadioTower" style="cursor:pointer;">📡</a></span>`;

        
        /** SCAVENGE */
        // const maxRad = 50;
        const currentRad = get('rad') || 0; // Retrieve current rad from storage
        // const radRegenRate = 1;             // Rad regenerated per interval
        // const regenIntervalMinutesRad = 5;    // Interval in minutes
        // const missingRad = maxRad - currentRad;
        // const timeToFullRadMinutes = Math.ceil((missingRad / radRegenRate) * regenIntervalMinutesRad);

        // let radTimeLeftFormatted = '';
        // if (timeToFullRadMinutes > 0) {
        //     const hours = Math.floor(timeToFullRadMinutes / 60);
        //     const minutes = timeToFullRadMinutes % 60;
        //     radTimeLeftFormatted = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
        // }
        // html += `<span class="zedhelper-timer-span">Scavenge: <a id="zhOpenScavenge" style="cursor:pointer;">${timeToFullRadMinutes > 0 ? 
        //     `<span class="red">${radTimeLeftFormatted}</span>`
        //     : `<span class="green">Ready</span>`
        // }</a></span>`;
        // html += `<span class="zedhelper-timer-span"><a id="zhOpenScavenge" style="cursor:pointer;"><img src="https://www.zed.city/assets/scrapyard-BrTM3-qI.jpg" style="width:20px;position:relative;top:5px;"></a></span>`;
        // html += `<span class="zedhelper-timer-span"><a id="zhOpenScavenge" style="cursor:pointer;">${currentRad < 10 ? `<span class="red">Scavenge</span>` : `<span class="green">Scavenge</span>`}</a></span>`;
        html += `<span class="zedhelper-timer-span"><a id="zhOpenScavenge" style="cursor:pointer;">${currentRad < 10 ? `<span class="red">🛢️</span>` : `<span class="green">🛢️</span>`}</a></span>`;
                
        /** RAID */
        const raidCooldownSecondsLeft = get('raidCooldownSecondsLeft');
        const raidCooldownTime = get('raidCooldownTime');
        if (raidCooldownSecondsLeft && raidCooldownTime) {
            timeDiff = (Date.now() - raidCooldownTime)/1000;
            timeLeft = raidCooldownSecondsLeft - Math.round(timeDiff);
            try {
                timeLeftFormatted = new Date(timeLeft * 1000).toISOString().substr(11, 5);
            } catch (error) {
                timeLeftFormatted = "00:00";
            }
        }
        html += `<span class="zedhelper-timer-span" tooltip="${timeLeftFormatted}"><a id="zhOpenRaid" style="cursor:pointer;">${timeLeft > 0 ? `<span class="red">🪖&nbsp;${timeLeftFormatted}</span>` : `<span class="green">🪖✅</span>`}</a></span>`;

        /** JUNK STORE */
        const junkStoreLimitSecondsLeft = get('junkStoreLimitSecondsLeft');
        const junkStoreLimitTime = get('junkStoreLimitTime');
        if (junkStoreLimitSecondsLeft && junkStoreLimitTime) {
            timeDiff = (Date.now() - junkStoreLimitTime)/1000;
            timeLeft = junkStoreLimitSecondsLeft - Math.round(timeDiff);
            try {
                timeLeftFormatted = new Date(timeLeft * 1000).toISOString().substr(11, 5);
            } catch (error) {
                timeLeftFormatted = "00:00";
            }
        }
        html += `<span class="zedhelper-timer-span"><a id="zhOpenJunkStore" style="cursor:pointer;">${timeLeft > 0 ? `<span class="red">🏪&nbsp;${timeLeftFormatted}</span>` : `<span class="green">🏪✅</span>`}</a></span>`;
        
        /** ZED MART */
        const zedMartLimitSecondsLeft = get('zedMartLimitSecondsLeft');
        const zedMartLimitTime = get('zedMartLimitTime');
        if (zedMartLimitSecondsLeft && zedMartLimitTime) {
            timeDiff = (Date.now() - zedMartLimitTime)/1000;
            timeLeft = zedMartLimitSecondsLeft - Math.round(timeDiff);
            try {
                timeLeftFormatted = new Date(timeLeft * 1000).toISOString().substr(11, 5);
            } catch (error) {
                timeLeftFormatted = "00:00";
            }
        }
        html += `<span class="zedhelper-timer-span"><a id="zhOpenZedMart" style="cursor:pointer;">${timeLeft > 0 ? `<span class="red">🏬&nbsp;${timeLeftFormatted}</span>` : `<span class="green">🏬✅</span>`}</a></span>`;

        html += `</div>`;

        timerBar.innerHTML = html;

        const selector = ".q-col-gutter-md.justify-center.items-center.currency-stats";
        log("Searching for statusbar...");
        waitForElement(selector).then((el) => {

            try {
                document.querySelectorAll('.zedhelper-icon-bar').entries().forEach((entry) => {
                    log("Remove entry of icon bar: " + entry[1]);
                    entry[1].remove();
                });
                document.querySelectorAll('.zedhelper-timer-bar').entries().forEach((entry) => {
                    log("Remove entry of timer bar: " + entry[1]);
                    entry[1].remove();
                });
            } catch (error) {
                // eat exception
            }
            log("Appending ZedHelper icon to statusbar + adding new bar for timers!");
            el.appendChild(zedHelperIcon);
            el.parentElement.appendChild(timerBar);

            // document.querySelector(selector).parentElement.appendChild(zedHelperIcon);

            /** Setup event listener to detect clicks on the timer buttons */
            setTimeout(() => {
                document.querySelector('#zhOpenGym').addEventListener('click', (event) => {
                    event.preventDefault();
                    openGym();
                });
                document.querySelector('#zhOpenRadioTower').addEventListener('click', (event) => {
                    event.preventDefault();
                    openRadioTower();
                });
                document.querySelector('#zhOpenScavenge').addEventListener('click', (event) => {
                    event.preventDefault();
                    openScavenge();
                });
                document.querySelector('#zhOpenRaid').addEventListener('click', (event) => {
                    event.preventDefault();
                    openRaid();
                });
                document.querySelector('#zhOpenJunkStore').addEventListener('click', (event) => {
                    event.preventDefault();
                    openJunkStore();
                });
                document.querySelector('#zhOpenZedMart').addEventListener('click', (event) => {
                    event.preventDefault();
                    openZedMart();
                });
            },10);

        });
    }
    function openGym() {
        log("Opening Gym...");
        // Navigate to Faction
        const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "stronghold");
        link.click();
        waitForElement('div.building-cont').then(() => {
            setTimeout(() => {
                const gymDiv = [...document.querySelectorAll("div.building-cont")].find(el => el.textContent.trim().includes("Gym"));
                gymDiv.click();
            },250);
        });
    }
    function openRadioTower() {
        log("Opening Radio Tower...");
        // Navigate to Faction
        const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "stronghold");
        link.click();
        waitForElement('div.building-cont').then(() => {
            setTimeout(() => {
                const link = [...document.querySelectorAll("div.building-cont")].find(el => el.textContent.trim().includes("Radio Tower"));
                link.click();
            },250);
        });
    }
    function openScavenge() {
        log("Opening Scavenge...");
        // Navigate to Faction
        const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "scavenge");
        link.click();
        waitForElement('.job-cont').then(() => {
            setTimeout(() => {
                const link = [...document.querySelectorAll(".job-cont")].find(el => el.textContent.includes("Scrapyard"));
                link.click();
            },250);
        });
    }
    function openRaid() {
        log("Opening Raid...");
        // Navigate to Faction
        const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "faction");
        link.click();
        // Then click on Raid
        waitForElement('a[href="/raids"]').then((el) => {
            el.click();
        });
    }
    function openJunkStore() {
        log("Opening Junk Store...");
        // Navigate to City
        const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "city");
        link.click();
        // Then click on Junk store
        waitForElement('a[data-cy="citymenu-junk-store"]').then((el) => {
            el.click();
        });
    }
    function openZedMart() {
        log("Opening Zed Mart...");
        // Navigate to City
        const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "city");
        link.click();
        // Then click on Zed Mart
        waitForElement('a[data-cy="citymenu-zed-mart"]').then((el) => {
            el.click();
        });
    }
    
    // addZedHelperIconAndTimerBar();









    /** Settings page */

    function showSettingsPage() {
        const selector = "#q-app > div > div.q-page-container > div";
        waitForElement(selector).then((el) => {
            el.style.top = "40%";
            el.style.width = "90%";
            el.style.border = "2px inset #333";
            el.style.padding = "10px";
            el.innerHTML = `
            <h3>ZedHelper Settings</h3>
            <p>Userscript written by <a href="https://www.zed.city/profile/12853">Kvassh</a><br>
            For any questions or feedback, please reach out to me in Zed City or Discord.</p>

            <br><br><hr><br>
            
            <div style="text-align:left;">
                <label>Enable extra nav menu? <input type="checkbox" id="extraNavMenu" name="extraNavMenu" value="true" ${get('extraNavMenu') === true | get('extraNavMenu') === 'true' ? 'checked' : ''}></label>
            </style>

            <br><br>
            <div id="zedhelper-settings-output" style="height:50px; display:block;"> </div>
            
            `;
            setTimeout(() => {
                document.querySelector("#extraNavMenu").addEventListener('change', (event) => {
                    set('extraNavMenu', event.target.checked);
                    document.querySelector('#zedhelper-settings-output').innerHTML = '<b class="green">Settings saved! &check;<br>You might need to refresh page for some settings like the extra nav menu.</b>';
                    setTimeout(() => {
                        document.querySelector('#zedhelper-settings-output').innerHTML = " ";
                    },1000);
                });
            }, 100);
        });
    }










    /** Gym functions */

    function autoPopulateTrainInput() {

        const energy = get("energy");
        if (energy > 5) {
            const trainsAvailable = Math.floor(energy/5);
            log(`Current energy: ${energy} - Autopopulating ${trainsAvailable} into the input fields`);

            waitForElement("input.q-field__native").then(() => {
                
                const inputs = document.querySelectorAll("input.q-field__native");
                for (let input of inputs) {
                    input.value = trainsAvailable;
                    input.dispatchEvent(new Event("input", { bubbles: true }));
                }

            });
        } else {
            log("Current energy is 5 or lower, don't autopopulate input fields");
        }
    }
                    






    /** Scavenge functions */
    function addBulkScavengeButtons() {
        const selector = "#q-app > div > div.q-page-container > main > div > div > div.full-width > div > div.q-mt-lg";
        waitForElement(selector).then(() => {
            // Add buttons for 1, 5, 10, 25, 50, 100 scavenges
            const container = document.querySelector(selector);
            const doScavengeBtn = document.querySelector("#q-app > div > div.q-page-container > main > div > div > div.full-width > div > div.q-mt-lg > button:nth-child(1)");

            const btn5 = document.createElement('button');
            btn5.innerHTML = `<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row"><span class="block">x5</span></span>`;
            btn5.classList = "q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-positive text-white q-btn--actionable q-focusable q-hoverable";
            btn5.style.margin = "5px";
            btn5.addEventListener('click', () => {
                console.log("Clicking 5 times...");
                for (let i = 0; i < 5; i++) {
                    doScavengeBtn.click();
                }
            });
            container.append(btn5);

            const btn30 = document.createElement('button');
            btn30.innerHTML = `<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row"><span class="block">x30</span></span>`;
            btn30.classList = "q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-positive text-white q-btn--actionable q-focusable q-hoverable";
            btn30.style.margin = "5px";
            btn30.addEventListener('click', () => {
                console.log("Clicking 30 times...");
                for (let i = 0; i < 30; i++) {
                    doScavengeBtn.click();
                }
            });
            container.append(btn30);

            const currentRad = get('rad') || 0;
            const btnMax = document.createElement('button');
            btnMax.innerHTML = `<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row"><span class="block">Rad (x${currentRad})</span></span>`;
            btnMax.classList = "q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-positive text-white q-btn--actionable q-focusable q-hoverable";
            btnMax.style.margin = "5px";
            btnMax.addEventListener('click', () => {
                console.log(`Current rad: ${currentRad} - Clicking ${currentRad} times...`);
                for (let i = 0; i < currentRad; i++) {
                    doScavengeBtn.click();
                }
            });
            container.append(btnMax);
        });
    }



    /** Radio Tower functions */
    function showTradeValues() {
        try {

            const timeDiff = get("radio_lastupdate") ? (Date.now() - get("radio_lastupdate"))/1000 : 0;
            log(`Trade values last updated: ${timeDiff} sec ago`);
            if (timeDiff > 60*60*12) {
                log("Trade values are old. Please visit the Radio Tower to cache new values.");

                const el = document.createElement('div');
                el.classList.add('radio-warning');
                waitForElement("div.overlay-cont").then(() => {
                    const container = document.querySelector("div.overlay-cont");
                    // document.querySelector("#q-app > div > div.q-page-container > main > div > div:nth-child(11) > div.overlay-cont > div > div > div > div > div.text-center.text-no-bg-light.subtext-large.q-my-md")
                    el.innerHTML = `Radio trades data are old - please refresh <a href="stronghold/2375019">Radio Tower</a> to cache new values.`;
                    container.prepend(el);
                });
                return;
            }

            const trades = JSON.parse(get(`tradeValues`));
            // [{"give":96,"return":460},{"give":1425,"return":11900},{"give":3000,"return":2380}]
            log("Current trades to show:");
            log(trades);

            waitForElement(".q-pa-md").then(() => {
                const tradeContainers = document.querySelectorAll(".q-pa-md");
                let i = 0;
                for (let tradeContainer of tradeContainers) {
                    const valueEl = document.createElement('div');
                    valueEl.classList.add('trade-value');
                    valueEl.innerHTML = `
                    <div style="float:left;">
                        ${trades[i].giveqty} items worth<br><span class="red">$</span> ${formatNumber(trades[i].give)}
                    </div>
                    <div style="float:right;">
                        ${trades[i].returnqty} items worth<br><span class="green">$</span> ${formatNumber(trades[i].return)}
                    </div>
                    <div style="clear:both;font-size:1.2rem;">
                        ${parseInt(trades[i].return) > parseInt(trades[i].give) ? '<span class="green">&check;</span>' : '<span class="red">&cross;</span>'}
                    </div>
                    `;
                    tradeContainer.appendChild(valueEl);
                    i++;
                }

                // Update radiotower last purchased date
                set(`radioTower_last_visited`, Date.now());
            });
        } catch(error) {
            log("No trade values found");
        }
    }

    function saveCurrentTradeValues(data) {
        try {
            const trades = [];
            for (let trade of data.items) {
                // trade -> vars -> items -> <item_requirement_1> -> codename/req_qty
                // trade -> vars -> output -> <item_list-1> -> codename/quantity
                let worthGive = 0;
                let worthReturn = 0;
                let qtyGive = 0;
                let qtyReturn = 0;
                const items = trade.vars.items;
                Object.keys(items).forEach( (key,val) => {
                    const marketValue = JSON.parse(get(`mv_${items[key].codename}`)).marketValue;
                    worthGive += (marketValue*items[key].req_qty);
                    qtyGive += items[key].req_qty;
                });
                const output = trade.vars.output;
                Object.keys(output).forEach( (key,val) => {
                    const marketValue = JSON.parse(get(`mv_${output[key].codename}`)).marketValue;
                    worthReturn += (marketValue*output[key].quantity);
                    qtyReturn += output[key].quantity;
                });
                log(`Trade: ${trade.name} - Give: ${worthGive} - Return: ${worthReturn}`);
                trades.push({ "give": worthGive, "return": worthReturn, "giveqty": qtyGive, "returnqty": qtyReturn });
            }
            set(`tradeValues`, JSON.stringify(trades));
        } catch(error) {
            log("Error saving trade values");
            set(`tradeValues`, null);
        }
    }



    /** Store functions */

    function autoPopulate360Items() {
        const selector = "input[type=number].q-placeholder";
        waitForElement(selector).then(() => {
            const el = document.querySelector(selector);
            el.value = 360;
            el.dispatchEvent(new Event("input", { bubbles: true }));
        });
    }
    function autoPopulateMaxItems() {
        const selector = "input[type=number].q-placeholder";
        waitForElement(selector).then(() => {
            const maxButton = [...document.querySelectorAll("button")].find(btn => btn.textContent.toLowerCase().includes("max"));
            maxButton.click();
            // const el = document.querySelector(selector);
            // el.value = 360;
            // el.dispatchEvent(new Event("input", { bubbles: true }));
        });
    }



    /** Functions related to market/inventory */

    // Function to process inventory items and add prices
    async function addMarketPrices() {

        const items = document.querySelectorAll('.item-row');

        if (!items) {
        log("No inventory items found. Check your selectors.");
        return;
        }

        const mvLastUpdateEl = document.createElement('div');
        mvLastUpdateEl.classList.add('zedhelper-inventory-warning');
        const mvLastUpdated = get('mv_lastupdate');
        if (mvLastUpdated) {
            const timeDiff = (Date.now() - mvLastUpdated)/1000;
            log(`Market values last updated: ${timeDiff} sec ago`);
            if (timeDiff > 60*60*24) {
                log("Market values are older than 24 hours. Please visit the market page to cache new values.");
                mvLastUpdateEl.innerHTML = `Market values are older than a day - please visit the <a href="market">Market</a> page to cache new values.`;
            }
        }
        else {
            log("Market values not cached. Please visit the market page to cache values.");
            mvLastUpdateEl.innerHTML = `
            Market value has not been cached yet.<br>
            Please visit the <a href="market">Market</a> page first to calculate worth on your inventory.
            `;
        }
        const selector = "#q-app > div > div.q-page-container > main > div > div:nth-child(2)";
        waitForElement(selector).then(() => {
            document.querySelector(selector).prepend(mvLastUpdateEl);
        });

        // Delete any existing market value elements
        const existingMarketValues = document.querySelectorAll('.market-price');
        for (let mvEl of existingMarketValues) {
            mvEl.remove();
        }

        for (let item of items) {

            const codename = getCodename(item.querySelector('.q-item__label').innerText);
            let qty = 1;
            try {
                qty = item.querySelector('.item-qty').innerText;
                if (qty.includes("%")) {
                    qty = 1;
                } else {
                    qty = parseInt(qty.replace(/[^0-9]/g, ''));
                }
            } catch (error) {
                // eat exception
            }
            if (Number.isNaN(qty)) {
                qty = 1;
            }
            log(`Adding market value for ${codename} x ${qty}`);
            
            let data = null;
            if(get(`mv_${codename}`)) {
                data = JSON.parse(get(`mv_${codename}`));
            } 

            const priceElement = document.createElement('span');
            priceElement.classList.add('market-price');
            
            if (data !== null) {
                const datetime = new Date(data.tz).toISOString();
                priceElement.innerHTML = `<span title="${datetime}">
<b class="green">$</b> ${formatNumber(data.marketValue * qty)} 
<small>(<b class="green">$</b> ${formatNumber(data.marketValue)})</small>
</span>`;
            } else {
                priceElement.innerHTML = `<span class="gray">N/A</span>`;
            }
            item.querySelector('.q-item__label').appendChild(priceElement);
        }

        // Setup interval to check if inventory list changes
        let firstItemRowCodename = "";
        try {
            firstItemRowCodename = getCodename(items[0].querySelector('.q-item__label').innerText);
        } catch (error) {
            // eat exception
        }

        checkForInventoryUpdates = setInterval(() => {
            let newItems = document.querySelectorAll('.item-row');
            if (newItems.length !== items.length) {
                log("Inventory list has changed. Updating prices...");
                clearInterval(checkForInventoryUpdates);
                checkForInventoryUpdates = null;
                addMarketPrices();
                return;
            }
            let newFirstItemRowCodename = ""; 
            try {
                newFirstItemRowCodename = getCodename(newItems[0].querySelector('.q-item__label').innerText);
            } catch (error) {
                // eat exception
            }
            if (firstItemRowCodename != newFirstItemRowCodename) {
                log("Inventory list has changed. Updating prices...");
                clearInterval(checkForInventoryUpdates);
                checkForInventoryUpdates = null;
                addMarketPrices();
                return;
            }
        },250);
    }
    function showNetworth() {
        const networthVendor = get(`mv_networth_vendor`) || 0;
        const networthMarket = get(`mv_networth_market`) || 0;
        const networthCash = get(`money`) || 0;
        const networth = parseInt(networthMarket) + parseInt(networthCash);

        const existingElement = document.querySelector('.zedhelper-networth');
        if (existingElement) {
            existingElement.remove8();
        }

        const el = document.createElement('div');
        el.classList.add('zedhelper-networth');
        
        el.innerHTML = `
        Networth:
        <span title="Value of items only if sold to vendor: $ ${formatNumber(networthVendor)}">
            <b class="green">$</b> ${formatNumber(networth)}
        </span>
        `;
        
        const selector = "#q-app > div > div.q-page-container > main > div > div:nth-child(2)";
        waitForElement(selector).then(() => {
            document.querySelector(selector).prepend(el);
        });
    }

    
})();

































/* EXAMPLE RESPONSE /getOffers:
    [
        {
            "name":"Advanced Tools",
            "codename":"advanced_tools",
            "type":"resources_craft_basic",
            "quantity":2,
            "value":10,
            "vars":{
                "buy":10,"sell":5,"desc":"","weight":"1","ash_value":"20"
            },
            "market_id":15490,
            "market_price":19500,
            "quantity_sold":3,
            "user":{
                "id":11703,"username":"bump","avatar":"","online":1739285402
            }
        },
    ]
*/

/* EXAMPLE RESPONSE /getMarket
{
    "items": 
    [
        {
            "name":"Advanced Tools",
            "codename":"advanced_tools",
            "type":"resources_craft_basic",
            "quantity":35,
            "value":10,
            "vars": {
                "buy":10,
                "sell":5,
                "desc":"",
                "weight":"1",
                "ash_value":"20"
            },
            "market_id":14020,
            "market_price":19500,
            "quantity_sold":0
        },      
    ]
}   

*/