DF Panels (BETA)

Panels script for Dead Frontier - Boss Timer, Auto Bank, Quick Service and Quick Buy

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         DF Panels (BETA)
// @namespace    http://tampermonkey.net/
// @version      0.2.3
// @description  Panels script for Dead Frontier - Boss Timer, Auto Bank, Quick Service and Quick Buy
// @author       D1N0 + Community scripts
// @exclude      https://fairview.deadfrontier.com/onlinezombiemmo/index.php?action=login2
// @exclude      https://fairview.deadfrontier.com/onlinezombiemmo/index.php?action=logout*
// @match        https://fairview.deadfrontier.com/onlinezombiemmo/index.php*
// @match        https://fairview.deadfrontier.com/onlinezombiemmo/
// @match        https://fairview.deadfrontier.com/onlinezombiemmo/DF3D/DF3D_InventoryPage.php*
// @icon         https://www.favicon.cc/favicon/336/1014/favicon.ico
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.xmlHttpRequest
// @connect      dfprofiler.com
// @run-at       document-idle
// @license      GPL-3.0-or-later
// ==/UserScript==

/*
 * Created by D1N0 and inspired by community scripts
 * DFP: https://www.dfprofiler.com/profile/view/12191879
 * ═══════════════════════════════════════════════════════════════════════════
 * DF PANELS - COMPREHENSIVE UTILITY PANELS FOR DEAD FRONTIER
 * ═══════════════════════════════════════════════════════════════════════════
 * 
 * WHAT THIS SCRIPT DOES:
 * ----------------------
 * Adds four utility panels to the Dead Frontier game interface:
 * 
 * 1. BOSS TIMER
 *    - Countdown to next hourly boss spawn
 *    - Special Devil Hound spawn alert (Inner City special spawn only)
 *    - Boss map button (opens dfprofiler.com in modal)
 * 
 * 2. AUTO BANK
 *    - Quick withdraw buttons (50k, 150k, 5M, All)
 *    - Quick deposit all button
 *    - Restores marketplace search when returning from bank
 * 
 * 3. QUICK SERVICE
 *    - One-click food/medical/armor repair in marketplace
 *    - All Services button (executes all three)
 *    - Smart item selection based on character level
 *    - Money check before redirect to marketplace
 * 
 * 4. QUICK BUY
 *    - Rapid purchase of common items (food, ammo, repair kits)
 *    - Right-click to favorite items (green highlight)
 *    - Auto-search and purchase
 * 
 * DATA STORAGE:
 * -------------
 * - Marketplace cache: GM.getValue/GM.setValue (30s cache, max 17 requests/30s)
 * - Search history: localStorage (last search term)
 * - Favorites: localStorage (highlighted QuickBuy items)
 * 
 * EXTERNAL API:
 * ------------
 * - dfprofiler.com/bossmap/json - Boss spawn data (read-only, public)
 * - NO user data is sent externally
 * - All game actions use Dead Frontier's own endpoints
 * 
 * SECURITY:
 * ---------
 * - NO credentials stored
 * - Uses game's built-in hash() function for requests
 * - Isolated to panel container areas only
 * - Open source - full code visible below
 * 
 * ═══════════════════════════════════════════════════════════════════════════
 */

(function() {
    'use strict';

    // Compatibility flag for other scripts
    window.BrowserImplant_QuickBuy = true;
    'use strict';

    window.BrowserImplant_QuickBuy = true;

    const origin = window.location.origin;
    const path = window.location.pathname;
    const params = new URLSearchParams(window.location.search);
    const returnPage = params.get('originPage');
    const currentPage = params.get('page') || '';

    // Special pages where only the Boss Timer panel is shown
    const isDF3D = path.includes('DF3D_InventoryPage.php');
    const isBattlefield = !isDF3D && currentPage === '21';
    const isBossTimerOnly = isDF3D || isBattlefield;

    // ═══════════════════════════════════════════════════════════════════════════
    // AUTOBANK: MARKETPLACE SEARCH RESTORATION
    // ═══════════════════════════════════════════════════════════════════════════
    // Restores the user's marketplace search query after returning from the bank.
    // This preserves workflow by remembering what they were searching for before banking. //
    ///////////////////////////////////////////

    if(sessionStorage.getItem('df_auto_restore')) {
        const last = localStorage.getItem('lastDFsearch');
        if(!last) {
            sessionStorage.removeItem('df_auto_restore');
        } else {
            const maxAttempts = 30;
            let attempts = 0;
            const tryRestore = () => {
                attempts++;
                const inp =
                    document.getElementById('searchField') ||
                    document.querySelector("input[name='searchField']") ||
                    document.querySelector('#marketSearch') ||
                    document.querySelector("input[type='text'][name='item_name']");
                const btn =
                    document.getElementById('makeSearch') ||
                    document.querySelector("#marketForm input[type='submit'], #marketForm input[type='button']");
                if(inp) {
                    inp.value = last;
                    inp.dispatchEvent(new Event('input', { bubbles: true }));
                }
                if(inp && btn) {
                    setTimeout(() => btn.click(), 80);
                    sessionStorage.removeItem('df_auto_restore');
                } else if(attempts < maxAttempts) {
                    setTimeout(tryRestore, 150);
                } else {
                    sessionStorage.removeItem('df_auto_restore');
                }
            };
            const startRestore = () => setTimeout(tryRestore, 100);
            if(document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', startRestore);
            } else {
                startRestore();
            }
        }
    }

    ///////////////////////////////////////////
    //  Autobank: Bank Page Handler          //
    ///////////////////////////////////////////

    if(currentPage === '15' && params.has('scripts')) {
        const action = params.get('scripts');
        window.addEventListener('load', () => {
            setTimeout(() => {
                if(action === 'withdraw') {
                    const amt = params.get('amount') || '50000';
                    const input = document.querySelector('#withdraw');
                    const btn = document.querySelector('#wBtn');
                    if(input && btn) {
                        input.value = amt;
                        input.setAttribute('value', amt);
                        ['input', 'change'].forEach(e => input.dispatchEvent(new Event(e, { bubbles: true })));
                        // eslint-disable-next-line no-undef
                        (typeof withdraw === 'function' ? withdraw() : btn.click());
                    }
                } else if(action === 'withdrawAll') {
                    // eslint-disable-next-line no-undef
                    if(typeof withdraw === 'function') withdraw(1);
                    else document.querySelector("button[onclick='withdraw(1);']")?.click();
                } else if(action === 'deposit') {
                    // eslint-disable-next-line no-undef
                    if(typeof deposit === 'function') deposit(1);
                    else document.querySelector("button[onclick='deposit(1);']")?.click();
                }
            }, 200);
            setTimeout(() => {
                sessionStorage.setItem('df_auto_restore', '1');
                const back = returnPage || '';
                window.location.replace(`${origin}${path}?page=${back}`);
            }, 500);
        });
        return;
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // QUICK SERVICE: GLOBAL VARIABLES
    // ═══════════════════════════════════════════════════════════════════════════
    // Storage for marketplace data, user info, and rate limiting.
    // All data is cached locally - nothing is sent to external servers.              //
    ///////////////////////////////////////////

    function getUserVars() { return unsafeWindow.userVars; }
    function getGlobalData() { return unsafeWindow.globalData; }

    var itemsDataBank = {};
    var servicesDataBank = {};
    var userData = {};
    var savedMarketData = {
        requestsIssued: 0,
        previousDataTimestamp: 0,
        previousItemTimestamp: {},
        previousServicesTimestamp: 0,
        itemsDataBank: {},
        servicesDataBank: {}
    };
    var REQUEST_LIMIT = 17;
    var helpWindow = null;

    var pendingRequests = {
        requestsNeeded: 0,
        requestsCompleted: 0,
        requesting: false
    };

    // ═══════════════════════════════════════════════════════════════════════════
    // UTILITY FUNCTIONS
    // ═══════════════════════════════════════════════════════════════════════════
    // Helper functions for API requests, data manipulation, and UI feedback.
    // makeRequest() uses the game's built-in hash() function for security.              //
    ///////////////////////////////////////////

    function capitalizeFirstLetter(string) {
        return string.charAt(0).toUpperCase() + string.slice(1);
    }

    function serializeObject(obj) {
        var pairs = [];
        for(let prop in obj) {
            if(!obj.hasOwnProperty(prop)) continue;
            pairs.push(prop + '=' + obj[prop]);
        }
        return pairs.join('&');
    }

    function makeRequest(requestUrl, requestParams, callbackFunc, callBackParams) {
        var userVars = getUserVars();
        requestParams.pagetime = userVars.pagetime;
        requestParams.templateID = "0";
        requestParams.sc = userVars.sc;
        requestParams.gv = 42;
        requestParams.userID = userVars.userID;
        requestParams.password = userVars.password;

        return new Promise((resolve) => {
            var xhttp = new XMLHttpRequest();
            var payload = null;
            xhttp.onreadystatechange = function() {
                if(this.readyState == 4 && this.status == 200) {
                    let callbackResponse = null;
                    if(callbackFunc != null) {
                        callbackResponse = callbackFunc(this.responseText, callBackParams);
                    }
                    if(callbackResponse == null) callbackResponse = true;
                    resolve(callbackResponse);
                }
            };
            payload = serializeObject(requestParams);
            xhttp.open("POST", requestUrl, true);
            xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
            xhttp.setRequestHeader("x-requested-with", "SilverScriptRequest");
            payload = "hash=" + unsafeWindow.hash(payload) + "&" + payload;
            xhttp.send(payload);
        });
    }

    function cloneObject(object) { return JSON.parse(JSON.stringify(object)); }

    function isAtLocation(location) {
        if(location === "marketplace") return window.location.href.indexOf("page=35") !== -1;
        if(location === "home") {
            var url = window.location.href;
            return url === "https://fairview.deadfrontier.com/onlinezombiemmo/" ||
                   url === "https://fairview.deadfrontier.com/onlinezombiemmo/index.php" ||
                   !!url.match(/https:\/\/fairview\.deadfrontier\.com\/onlinezombiemmo\/index\.php(\?)?$/);
        }
        return false;
    }

    function findLastEmptyGenericSlot(slotType) {
        var userVars = getUserVars();
        for(let i = userVars["DFSTATS_df_" + slotType + "slots"]; i >= 1; i--) {
            if(userVars["DFSTATS_df_" + slotType + i + "_type"] === "") return i;
        }
        return false;
    }

    function addPendingRequest() {
        pendingRequests.requestsNeeded += 1;
        pendingRequests.requesting = true;
    }

    function completePendingRequest() {
        pendingRequests.requestsCompleted += 1;
        if(pendingRequests.requestsCompleted >= pendingRequests.requestsNeeded) {
            pendingRequests.requestsNeeded = 0;
            pendingRequests.requestsCompleted = 0;
            pendingRequests.requesting = false;
        }
    }

    function havePendingRequestsCompleted() { return !pendingRequests.requesting; }

    // Red warning box — used for insufficient funds
    function showMoneyWarning() {
        if(document.getElementById("qsMoneyWarning")) return;
        var overlay = document.createElement("div");
        overlay.id = "qsMoneyWarning";
        overlay.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:99999;display:flex;justify-content:center;align-items:center;";

        var box = document.createElement("div");
        box.style.cssText = "background:#1a0000;border:3px solid #8b0000;border-radius:5px;padding:20px 40px;text-align:center;min-width:300px;box-shadow:0 0 20px rgba(139,0,0,0.5);";

        var title = document.createElement("div");
        title.textContent = "INSUFFICIENT FUNDS";
        title.style.cssText = "color:#ff0000;font-size:24px;font-weight:bold;font-family:Arial,sans-serif;margin-bottom:20px;text-shadow:2px 2px 4px rgba(0,0,0,0.8);";

        var sub = document.createElement("div");
        sub.textContent = "You don't have enough money for this action";
        sub.style.cssText = "color:#cccccc;font-size:14px;margin-bottom:20px;";

        var okBtn = document.createElement("button");
        okBtn.textContent = "OK";
        okBtn.style.cssText = "background:#8b0000;color:#fff;border:2px solid #ff0000;padding:10px 30px;font-size:16px;font-weight:bold;cursor:pointer;border-radius:3px;";
        okBtn.onclick = function() { document.body.removeChild(overlay); };

        box.appendChild(title);
        box.appendChild(sub);
        box.appendChild(okBtn);
        overlay.appendChild(box);
        document.body.appendChild(overlay);

        if(unsafeWindow.playSound) unsafeWindow.playSound("error");
        setTimeout(function() {
            if(document.getElementById("qsMoneyWarning")) okBtn.click();
        }, 3000);
    }

    // Green warning box — used when a service is already at maximum
    function showAlreadyFullWarning(message) {
        if(document.getElementById("qsAlreadyFullWarning")) return;
        var overlay = document.createElement("div");
        overlay.id = "qsAlreadyFullWarning";
        overlay.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:99999;display:flex;justify-content:center;align-items:center;";

        var box = document.createElement("div");
        box.style.cssText = "background:#001a05;border:3px solid #1a7a30;border-radius:5px;padding:20px 40px;text-align:center;min-width:300px;box-shadow:0 0 20px rgba(0,150,50,0.4);";

        var title = document.createElement("div");
        title.textContent = message;
        title.style.cssText = "color:#44ff88;font-size:20px;font-weight:bold;font-family:Arial,sans-serif;margin-bottom:20px;text-shadow:0 0 10px rgba(0,255,80,0.6),2px 2px 4px rgba(0,0,0,0.8);";

        var okBtn = document.createElement("button");
        okBtn.textContent = "OK";
        okBtn.style.cssText = "background:#0a5a1a;color:#fff;border:2px solid #44ff88;padding:10px 30px;font-size:16px;font-weight:bold;cursor:pointer;border-radius:3px;";
        okBtn.onclick = function() { document.body.removeChild(overlay); };

        box.appendChild(title);
        box.appendChild(okBtn);
        overlay.appendChild(box);
        document.body.appendChild(overlay);

        setTimeout(function() {
            if(document.getElementById("qsAlreadyFullWarning")) okBtn.click();
        }, 3000);
    }

    function updateInventoryData(inventoryData) {
        unsafeWindow.updateIntoArr(unsafeWindow.flshToArr(inventoryData, "DFSTATS_"), unsafeWindow.userVars);
        unsafeWindow.populateInventory();
        unsafeWindow.populateCharacterInventory();
        unsafeWindow.updateAllFields();
    }

    function closeHelpWindowPage() {
        if(helpWindow) {
            helpWindow.parentNode.style.display = "none";
            helpWindow.innerHTML = "";
        }
        unsafeWindow.pageLock = false;
    }

    function showLoadingWindow() {
        if(helpWindow) {
            helpWindow.innerHTML = "<div style='text-align:center'>Loading, please wait...</div>";
            helpWindow.parentNode.style.display = "block";
        }
        unsafeWindow.pageLock = true;
    }

    ///////////////////////////////////////////
    //  QuickService: Init Data              //
    ///////////////////////////////////////////

    function initUserData() {
        var userVars = getUserVars();
        if(!userVars || !userVars.DFSTATS_df_tradezone) return;
        userData.tradezone = userVars.DFSTATS_df_tradezone;
    }

    function addItemToDatabank(flashType, quantity) {
        var globalData = getGlobalData();
        var item = {};
        item.id = flashType;
        item.extraInfo = "";
        item.flashType = flashType;
        item.trades = [];

        if(item.id && item.id !== "") {
            if(item.id.indexOf("_") !== -1) {
                item.extraInfo = capitalizeFirstLetter(item.id.split("_")[1]);
                item.id = item.id.split("_")[0];
            }
            var itemGlobData = globalData[item.id];
            item.name = itemGlobData.name;
            item.quantity = quantity < 1 ? 1 : quantity;

            if(itemGlobData.needcook == "1" && item.extraInfo !== "Cooked") {
                item.profession = "Chef";
                item.level = itemGlobData.level;
                item.professionLevel = item.level - 5;
            } else if(itemGlobData.needdoctor == "1") {
                item.profession = "Doctor";
                item.level = itemGlobData.level;
                item.professionLevel = item.level - 5;
            }
            if(item.extraInfo === "Cooked") {
                item.id = item.id + "_cooked";
                item.name = "Cooked " + item.name;
            }
            if(itemsDataBank[item.id] == null) itemsDataBank[item.id] = item;
        }
        return item;
    }

    async function loadSavedMarketData() {
        savedMarketData = JSON.parse(await GM.getValue("savedMarketData", JSON.stringify(savedMarketData)));
        if(savedMarketData.previousItemTimestamp == undefined) savedMarketData.previousItemTimestamp = {};
        itemsDataBank = savedMarketData.itemsDataBank;
        servicesDataBank = savedMarketData.servicesDataBank;
    }

    async function saveMarketData() {
        savedMarketData.itemsDataBank = itemsDataBank;
        savedMarketData.servicesDataBank = servicesDataBank;
        await GM.setValue("savedMarketData", JSON.stringify(savedMarketData));
    }

    ///////////////////////////////////////////
    //  QuickService: Market Requests        //
    ///////////////////////////////////////////

    function requestItem(dataBankItem) {
        if(Date.now() < savedMarketData.previousDataTimestamp + 30000) {
            if(savedMarketData.previousItemTimestamp[dataBankItem.id] != undefined &&
               Date.now() < savedMarketData.previousItemTimestamp[dataBankItem.id] + 30000 &&
               dataBankItem.rawServerResponse != undefined && dataBankItem.rawServerResponse !== "") {
                return true;
            }
            if(savedMarketData.requestsIssued < REQUEST_LIMIT) {
                savedMarketData.requestsIssued += 1;
            } else {
                return false;
            }
        } else {
            savedMarketData.previousDataTimestamp = Date.now();
            savedMarketData.requestsIssued = 0;
        }
        savedMarketData.previousItemTimestamp[dataBankItem.id] = Date.now();
        addPendingRequest();

        var reqParams = {};
        reqParams.tradezone = userData.tradezone;
        reqParams.searchname = encodeURI(dataBankItem.name.substring(0, 15));
        reqParams.category = '';
        reqParams.profession = '';
        reqParams.memID = '';
        reqParams.searchtype = "buyinglistitemname";
        reqParams.search = "trades";

        let cb = function(responseText) {
            dataBankItem.rawServerResponse = responseText;
            if(responseText !== "") {
                itemsDataBank[dataBankItem.id].trades = [];
                var maxTrades = [...responseText.matchAll(new RegExp("tradelist_[0-9]+_id_member=", "g"))].length;
                if(responseText.indexOf("tradelist_maxresults=0") === -1 && maxTrades > 0) {
                    var trade = {};
                    trade.tradeID = parseInt(responseText.match(new RegExp("tradelist_0_trade_id=[0-9]+&"))[0].split("=")[1].match(/[0-9]+/)[0]);
                    trade.price = parseInt(responseText.match(new RegExp("tradelist_0_price=[0-9]+&"))[0].split("=")[1].match(/[0-9]+/)[0]);
                    itemsDataBank[dataBankItem.id].trades.push(trade);
                }
            }
            completePendingRequest();
            return true;
        };
        return makeRequest("https://fairview.deadfrontier.com/onlinezombiemmo/trade_search.php", reqParams, cb, null);
    }

    function buyItem(itemId) {
        if(itemsDataBank[itemId] == null || itemsDataBank[itemId].trades.length === 0) return false;
        var itemBuynum = itemsDataBank[itemId].trades[0].tradeID;
        var itemPrice = itemsDataBank[itemId].trades[0].price;
        if(itemBuynum == null) return false;

        var reqParams = {};
        reqParams.searchtype = "buyinglistitemname";
        reqParams.creditsnum = "undefined";
        reqParams.buynum = itemBuynum;
        reqParams.renameto = "undefined`undefined";
        reqParams.expected_itemprice = itemPrice;
        reqParams.expected_itemtype2 = "";
        reqParams.expected_itemtype = "";
        reqParams.itemnum2 = "0";
        reqParams.itemnum = "0";
        reqParams.price = "0";
        reqParams.action = "newbuy";

        let cb = function(responseText) {
            if(responseText.length < 32) {
                itemsDataBank[itemId].trades.shift();
                if(itemsDataBank[itemId].trades.length > 0) buyItem(itemId);
                else return false;
            } else {
                unsafeWindow.playSound("buysell");
                updateInventoryData(responseText);
            }
        };
        return makeRequest("https://fairview.deadfrontier.com/onlinezombiemmo/inventory_new.php", reqParams, cb, null);
    }

    function refreshServicesDataBank() {
        if(!isAtLocation("marketplace")) return;
        if(Date.now() < savedMarketData.previousServicesTimestamp + 30000) return;
        savedMarketData.previousServicesTimestamp = Date.now();
        servicesDataBank = {
            Chef: {name: "Chef"},
            Doctor: {name: "Doctor"},
            Engineer: {name: "Engineer"}
        };
        for(let serviceName in servicesDataBank) {
            addPendingRequest();
            var svc = servicesDataBank[serviceName];
            var reqParams = {};
            reqParams.tradezone = userData.tradezone;
            reqParams.searchname = "";
            reqParams.category = "";
            reqParams.profession = encodeURI(svc.name.substring(0, 15));
            reqParams.memID = "";
            reqParams.searchtype = "buyinglist";
            reqParams.search = "services";

            let cb = (function(dataBankService) {
                return function(responseText) {
                    var responseLength = [...responseText.matchAll(new RegExp("tradelist_[0-9]+_id_member=", "g"))].length;
                    if(responseText !== "") {
                        for(let i = 0; i < responseLength; i++) {
                            let serviceLevel = parseInt(responseText.match(new RegExp("tradelist_" + i + "_level=[0-9]+&"))[0].split("=")[1].match(/[0-9]+/)[0]);
                            if(dataBankService[serviceLevel] === undefined) dataBankService[serviceLevel] = [];
                            let service = {};
                            service.userID = parseInt(responseText.match(new RegExp("tradelist_" + i + "_id_member=[0-9]+&"))[0].split("=")[1].match(/[0-9]+/)[0]);
                            service.price = parseInt(responseText.match(new RegExp("tradelist_" + i + "_price=[0-9]+&"))[0].split("=")[1].match(/[0-9]+/)[0]);
                            dataBankService[serviceLevel].push(service);
                        }
                    }
                    completePendingRequest();
                };
            })(svc);

            makeRequest("https://fairview.deadfrontier.com/onlinezombiemmo/trade_search.php", reqParams, cb, null);
        }
    }

    function buyService(slotNumber, profession, professionLevel) {
        if(servicesDataBank[profession] == null ||
           servicesDataBank[profession][professionLevel] === undefined ||
           servicesDataBank[profession][professionLevel].length === 0) return false;

        var serviceBuynum = servicesDataBank[profession][professionLevel][0].userID;
        var servicePrice = servicesDataBank[profession][professionLevel][0].price;
        var serviceAction = profession === "Engineer" ? "buyrepair" : profession === "Chef" ? "buycook" : "buyadminister";
        unsafeWindow.pageLock = true;

        var reqParams = {};
        reqParams.creditsnum = "0";
        reqParams.buynum = serviceBuynum;
        reqParams.renameto = "undefined`undefined";
        reqParams.expected_itemprice = servicePrice;
        reqParams.expected_itemtype2 = "";
        reqParams.expected_itemtype = "";
        reqParams.itemnum2 = "0";
        reqParams.itemnum = slotNumber;
        reqParams.price = "0";
        reqParams.action = serviceAction;

        let cb = function(responseText) {
            unsafeWindow.pageLock = false;
            if(responseText.length < 32) {
                servicesDataBank[profession][professionLevel].shift();
                if(servicesDataBank[profession][professionLevel].length > 0) buyService(slotNumber, profession, professionLevel);
                else refreshServicesDataBank();
            } else {
                var soundEffect = profession === "Engineer" ? "repair" : profession === "Chef" ? "cook" : "heal";
                unsafeWindow.playSound(soundEffect);
                updateInventoryData(responseText);
                return true;
            }
        };
        return makeRequest("https://fairview.deadfrontier.com/onlinezombiemmo/inventory_new.php", reqParams, cb, null);
    }

    ///////////////////////////////////////////
    //  QuickService: Level Lists            //
    ///////////////////////////////////////////

    function getLevelAppropriateMedicalTypeList() {
        var playerLevel = getUserVars().DFSTATS_df_level;
        if(playerLevel < 11) return ["steristrips", "plasters"];
        if(playerLevel < 21) return ["antisepticspray", "antibiotics"];
        if(playerLevel < 31) return ["bandages"];
        if(playerLevel < 41) return ["morphine"];
        if(playerLevel < 71) return ["nerotonin"];
        return ["nerotonin8b", "steroids"];
    }

    function getBestMedicalAndAdministerNeed() {
        var userVars = getUserVars();
        var globalData = getGlobalData();
        var damagePercentage = (userVars.DFSTATS_df_hpmax / userVars.DFSTATS_df_hpcurrent) * 100;
        var medList = getLevelAppropriateMedicalTypeList();
        var chosenMed = "";
        var needAdminister = true;

        for(let medType of medList) {
            let adminhealthrestore = parseInt(globalData[medType].healthrestore) * 3;
            if(chosenMed !== "") {
                if(adminhealthrestore >= damagePercentage) chosenMed = medType;
                else break;
            } else {
                chosenMed = medType;
            }
        }
        for(let medType of medList) {
            if(parseInt(globalData[medType].healthrestore) >= damagePercentage) {
                chosenMed = medType;
                needAdminister = false;
            } else {
                break;
            }
        }
        return [chosenMed, needAdminister];
    }

    function getLevelAppropriateFoodTypeList() {
        var playerLevel = getUserVars().DFSTATS_df_level;
        if(playerLevel < 11) return ["millet_cooked", "beer"];
        if(playerLevel < 21) return ["hotdogs_cooked", "bakedbeans_cooked"];
        if(playerLevel < 31) return ["potatoes_cooked", "tuna_cooked"];
        if(playerLevel < 41) return ["eggs_cooked", "salmon_cooked", "oats_cooked"];
        if(playerLevel < 71) return ["caviar_cooked", "mixednuts_cooked", "redwine"];
        return ["driedtruffles_cooked", "whiskey"];
    }

    function getBestFoodType() {
        var userVars = getUserVars();
        var globalData = getGlobalData();
        var hunger = 50 - parseInt(userVars.DFSTATS_df_hungerhp);
        var foodList = getLevelAppropriateFoodTypeList();
        var chosenFood = "";

        for(let foodType of foodList) {
            let parts = foodType.split("_");
            let foodItem = globalData[parts[0]];
            let actualFoodrestore = parts[1] !== undefined ? parseInt(foodItem.foodrestore) * 3 : parseInt(foodItem.foodrestore);
            if(chosenFood !== "") {
                if(actualFoodrestore >= hunger) chosenFood = foodType;
                else break;
            } else {
                chosenFood = foodType;
            }
        }
        return chosenFood;
    }

    ///////////////////////////////////////////
    //  QuickService: Actions               //
    ///////////////////////////////////////////

    // Redirects to marketplace if not already there, saving a pending action to resume on arrival
    async function qsGoToMarketplaceIfNeeded(type) {
        if(!isAtLocation("marketplace")) {
            localStorage.setItem("quickServicePending", JSON.stringify({ type, timestamp: Date.now() }));
            window.location.href = "https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=35";
            return false;
        }
        return true;
    }

    async function qsDoRepair(silent) {
        if(!havePendingRequestsCompleted()) return;
        if(!(await qsGoToMarketplaceIfNeeded("armor"))) return;

        var userVars = getUserVars();
        var globalData = getGlobalData();
        var armorType = userVars.DFSTATS_df_armourtype.split("_")[0];

        // Check if armor is already fully repaired
        if(armorType === '' || parseInt(userVars.DFSTATS_df_armourhp) >= parseInt(userVars.DFSTATS_df_armourhpmax)) {
            if(!silent) showAlreadyFullWarning("YOUR ARMOR IS ALREADY REPAIRED");
            return;
        }

        var repairLevel = parseInt(globalData[armorType].shop_level) - 5;
        if(!servicesDataBank.Engineer || !servicesDataBank.Engineer[repairLevel] || !servicesDataBank.Engineer[repairLevel][0]) return;
        if(servicesDataBank.Engineer[repairLevel][0].price > userVars.DFSTATS_df_cash) { showMoneyWarning(); return; }

        var slotNumber = findLastEmptyGenericSlot("inv");
        if(slotNumber === false) return;

        closeHelpWindowPage();
        showLoadingWindow();

        var chainParams = {};
        chainParams.unequip = {};
        chainParams.unequip.creditsnum = userVars.DFSTATS_df_credits;
        chainParams.unequip.buynum = "0";
        chainParams.unequip.renameto = "undefined`undefined";
        chainParams.unequip.expected_itemprice = "-1";
        chainParams.unequip.price = unsafeWindow.getUpgradePrice();
        chainParams.equip = cloneObject(chainParams.unequip);

        chainParams.unequip.expected_itemtype2 = userVars.DFSTATS_df_armourtype;
        chainParams.unequip.expected_itemtype = "";
        chainParams.unequip.itemnum2 = 34;
        chainParams.unequip.itemnum = slotNumber;
        chainParams.unequip.action = "newequip";

        chainParams.equip.expected_itemtype2 = "";
        chainParams.equip.expected_itemtype = userVars.DFSTATS_df_armourtype;
        chainParams.equip.itemnum2 = 34;
        chainParams.equip.itemnum = slotNumber;
        chainParams.equip.action = "newequip";

        await makeRequest("https://fairview.deadfrontier.com/onlinezombiemmo/inventory_new.php", chainParams.unequip, updateInventoryData, null);
        await buyService(slotNumber, "Engineer", repairLevel);
        await makeRequest("https://fairview.deadfrontier.com/onlinezombiemmo/inventory_new.php", chainParams.equip, updateInventoryData, null);
        closeHelpWindowPage();
    }

    async function qsDoMedical(silent) {
        if(!havePendingRequestsCompleted()) return;
        if(!(await qsGoToMarketplaceIfNeeded("medical"))) return;

        var userVars = getUserVars();
        // Compare against the character's actual hpmax
        if(parseInt(userVars.DFSTATS_df_hpcurrent) >= parseInt(userVars.DFSTATS_df_hpmax)) {
            if(!silent) showAlreadyFullWarning("YOU ARE ALREADY HEALTHY");
            return;
        }

        var result = getBestMedicalAndAdministerNeed();
        var itemId = result[0];
        var needsAdminister = result[1];

        if(!itemsDataBank[itemId]) addItemToDatabank(itemId, 1);
        var requestResult = await requestItem(itemsDataBank[itemId]);
        if(!requestResult) { alert("Rate limited. Try again in a minute."); return; }

        if(needsAdminister && (!servicesDataBank.Doctor || !servicesDataBank.Doctor[itemsDataBank[itemId].professionLevel] || !servicesDataBank.Doctor[itemsDataBank[itemId].professionLevel][0])) return;
        if(!itemsDataBank[itemId].trades || itemsDataBank[itemId].trades.length === 0) return;

        var itemPrice = itemsDataBank[itemId].trades[0].price;
        if(needsAdminister) itemPrice += servicesDataBank.Doctor[itemsDataBank[itemId].professionLevel][0].price;
        if(itemPrice > userVars.DFSTATS_df_cash) { showMoneyWarning(); return; }

        var slotNumber = findLastEmptyGenericSlot("inv");
        if(slotNumber === false) return;

        closeHelpWindowPage();
        showLoadingWindow();

        var buyResult = await buyItem(itemId);
        if(!buyResult) { closeHelpWindowPage(); return; }

        if(needsAdminister) {
            await buyService(slotNumber, "Doctor", itemsDataBank[itemId].professionLevel);
        } else {
            var medParams = {};
            medParams.creditsnum = "0";
            medParams.buynum = "0";
            medParams.renameto = "undefined`undefined";
            medParams.expected_itemprice = "-1";
            medParams.expected_itemtype2 = "";
            medParams.expected_itemtype = itemId;
            medParams.itemnum2 = "0";
            medParams.itemnum = slotNumber;
            medParams.price = "0";
            medParams.action = "newuse";
            await makeRequest("https://fairview.deadfrontier.com/onlinezombiemmo/inventory_new.php", medParams, updateInventoryData, null);
            unsafeWindow.playSound("heal");
        }
        closeHelpWindowPage();
    }

    async function qsDoFood(silent) {
        if(!havePendingRequestsCompleted()) return;
        if(!(await qsGoToMarketplaceIfNeeded("food"))) return;

        var userVars = getUserVars();
        // hungerhp 50 = full nourishment
        if(parseInt(userVars.DFSTATS_df_hungerhp) >= 50) {
            if(!silent) showAlreadyFullWarning("YOU ARE ALREADY NOURISHED");
            return;
        }

        var itemId = getBestFoodType();
        if(!itemsDataBank[itemId]) addItemToDatabank(itemId, 1);
        var requestResult = await requestItem(itemsDataBank[itemId]);
        if(!requestResult) { alert("Rate limited. Try again in a minute."); return; }
        if(!itemsDataBank[itemId].trades || itemsDataBank[itemId].trades.length === 0) return;

        if(itemsDataBank[itemId].trades[0].price > userVars.DFSTATS_df_cash) { showMoneyWarning(); return; }

        var slotNumber = findLastEmptyGenericSlot("inv");
        if(slotNumber === false) return;

        closeHelpWindowPage();
        showLoadingWindow();

        var buyResult = await buyItem(itemId);
        if(!buyResult) { closeHelpWindowPage(); return; }

        var foodParams = {};
        foodParams.creditsnum = "0";
        foodParams.buynum = "0";
        foodParams.renameto = "undefined`undefined";
        foodParams.expected_itemprice = "-1";
        foodParams.expected_itemtype2 = "";
        foodParams.expected_itemtype = itemId;
        foodParams.itemnum2 = "0";
        foodParams.itemnum = slotNumber;
        foodParams.price = "0";
        foodParams.action = "newconsume";
        await makeRequest("https://fairview.deadfrontier.com/onlinezombiemmo/inventory_new.php", foodParams, updateInventoryData, null);
        unsafeWindow.playSound("eat");
        closeHelpWindowPage();
    }

    // Runs all three services sequentially, skipping any that are already at maximum.
    // Shows a green warning if everything is already full.
    // Checks if user has enough cash BEFORE redirecting to marketplace.
    // Guards against userVars being unavailable (e.g. on the home page before the game loads).
    async function qsDoAll() {
        var userVars = getUserVars();

        // If userVars is available, check fullness and cash BEFORE doing anything
        if(userVars) {
            var armorType = userVars.DFSTATS_df_armourtype ? userVars.DFSTATS_df_armourtype.split("_")[0] : '';
            var foodFull = parseInt(userVars.DFSTATS_df_hungerhp) >= 50;
            var healthFull = parseInt(userVars.DFSTATS_df_hpcurrent) >= parseInt(userVars.DFSTATS_df_hpmax);
            var armorFull = armorType === '' || parseInt(userVars.DFSTATS_df_armourhp) >= parseInt(userVars.DFSTATS_df_armourhpmax);

            if(foodFull && healthFull && armorFull) {
                showAlreadyFullWarning("EVERYTHING IS ALREADY AT MAXIMUM");
                return;
            }

            // Check if user has any money at all — show warning immediately if broke
            var cash = parseInt(userVars.DFSTATS_df_cash);
            if(cash <= 0) {
                showMoneyWarning();
                return;
            }
        }

        // If not at marketplace, redirect there with pending 'all' action
        if(!isAtLocation("marketplace")) {
            localStorage.setItem("quickServicePending", JSON.stringify({ type: "all", timestamp: Date.now() }));
            window.location.href = "https://fairview.deadfrontier.com/onlinezombiemmo/index.php?page=35";
            return;
        }

        // At marketplace — run all services sequentially with silent=true to suppress individual warnings
        await qsDoFood(true);
        await qsDoMedical(true);
        await qsDoRepair(true);
    }

    function checkPendingAction() {
        if(!isAtLocation("marketplace")) return;
        var pendingData = localStorage.getItem("quickServicePending");
        if(!pendingData) return;
        try {
            var pending = JSON.parse(pendingData);
            if(Date.now() - pending.timestamp > 5000) { localStorage.removeItem("quickServicePending"); return; }
            localStorage.removeItem("quickServicePending");
            setTimeout(async function() {
                if(pending.type === "armor") await qsDoRepair(false);
                else if(pending.type === "medical") await qsDoMedical(false);
                else if(pending.type === "food") await qsDoFood(false);
                else if(pending.type === "all") await qsDoAll();
            }, 1000);
        } catch(e) {
            localStorage.removeItem("quickServicePending");
        }
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // PANEL STYLING
    // ═══════════════════════════════════════════════════════════════════════════
    // All panel styles follow Dead Frontier's dark/orange theme.
    // Panels are designed to blend seamlessly with the game's existing UI.                                  //
    ///////////////////////////////////////////

    var css = `
        .df-panel {
            position: absolute;
            z-index: 10000;
            font-family: 'Arial Black', 'Arial Bold', Arial, sans-serif;
        }

        .df-panel-inner {
            background: linear-gradient(180deg, #1a0a00 0%, #0d0500 40%, #120800 100%);
            border: 1px solid #5a1a00;
            box-shadow:
                0 4px 16px rgba(0,0,0,0.9),
                inset 0 1px 0 rgba(120,50,0,0.3),
                0 0 20px rgba(0,0,0,0.6);
        }

        .df-panel-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 7px 10px 7px;
            background: #1a0800;
            cursor: default;
        }

        /* Divider line — real HTML element instead of CSS border (immune to sub-pixel zoom artifacts) */
        .df-panel-divider {
            height: 1px;
            background: #5a1a00;
            font-size: 0;
            line-height: 0;
        }

        .df-panel-title {
            color: #e87030;
            font-size: 10px;
            font-weight: 900;
            letter-spacing: 1px;
            text-shadow: 0 0 10px rgba(255,120,0,0.7), 1px 1px 2px rgba(0,0,0,0.9);
        }

        .df-panel-collapse {
            background: none;
            border: 1px solid #5a1a00;
            color: #b85a20;
            font-family: 'Arial Black', Arial, sans-serif;
            font-size: 9px;
            font-weight: 900;
            width: 18px;
            height: 18px;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 0;
            line-height: 1;
            text-shadow: 0 0 6px rgba(200,80,0,0.5);
            transition: all 0.12s ease;
        }

        .df-panel-collapse:hover {
            background: rgba(100,30,0,0.4);
            border-color: #aa4500;
            color: #e87030;
        }

        .df-panel-body {
            padding: 8px;
            background: linear-gradient(180deg, #1a0a00 0%, #0d0500 50%, #120800 100%);
        }

        .df-btn {
            background: linear-gradient(180deg, #2a0e00 0%, #1a0800 50%, #200b00 100%);
            border: 1px solid #4a1500;
            border-top-color: #6a2500;
            border-bottom-color: #300800;
            color: #b85a20;
            font-family: 'Arial Black', 'Arial Bold', Arial, sans-serif;
            font-size: 9px;
            font-weight: 900;
            letter-spacing: 0.5px;
            padding: 7px 8px;
            cursor: pointer;
            white-space: nowrap;
            text-shadow: 0 0 8px rgba(200,80,0,0.5), 1px 1px 2px rgba(0,0,0,0.9);
            box-shadow:
                inset 0 1px 0 rgba(150,60,0,0.2),
                inset 0 -1px 0 rgba(0,0,0,0.4),
                0 1px 3px rgba(0,0,0,0.6);
            transition: all 0.12s ease;
            text-decoration: none;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            position: relative;
            overflow: hidden;
        }

        .df-btn::before {
            content: '';
            position: absolute;
            top: 0; left: 0; right: 0;
            height: 40%;
            background: linear-gradient(180deg, rgba(180,70,0,0.08), transparent);
            pointer-events: none;
        }

        .df-btn:hover {
            background: linear-gradient(180deg, #3d1500 0%, #2a0d00 50%, #320f00 100%);
            border-color: #8a3000;
            border-top-color: #aa4500;
            color: #e87030;
            text-shadow: 0 0 12px rgba(255,120,0,0.8), 1px 1px 2px rgba(0,0,0,0.9);
            box-shadow:
                inset 0 1px 0 rgba(200,80,0,0.3),
                inset 0 -1px 0 rgba(0,0,0,0.4),
                0 1px 6px rgba(150,50,0,0.4);
        }

        .df-separator {
            height: 1px;
            background: linear-gradient(90deg, transparent, #5a1a00, transparent);
            margin: 4px 0;
        }

        /* ==================== BOSS MAP MODAL ==================== */
        #bm-overlay {
            position: fixed;
            inset: 0;
            background: rgba(0, 0, 0, 0.75);
            z-index: 999990;
            display: flex;
            align-items: center;
            justify-content: center;
            animation: bm-fade-in 0.15s ease;
        }

        @keyframes bm-fade-in {
            from { opacity: 0; }
            to   { opacity: 1; }
        }

        #bm-modal {
            position: relative;
            width: min(1100px, 96vw);
            height: min(780px, 90vh);
            background: linear-gradient(180deg, #1a0a00 0%, #0d0500 40%, #120800 100%);
            border: 2px solid #5a1a00;
            box-shadow:
                0 0 40px rgba(0,0,0,0.95),
                0 0 60px rgba(100,30,0,0.3),
                inset 0 1px 0 rgba(120,50,0,0.3);
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }

        #bm-modal-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 8px 12px;
            background: #1a0800;
            flex-shrink: 0;
        }

        #bm-modal-divider {
            height: 1px;
            background: #5a1a00;
            font-size: 0;
            line-height: 0;
            flex-shrink: 0;
        }

        #bm-modal-title {
            color: #e87030;
            font-family: 'Arial Black', 'Arial Bold', Arial, sans-serif;
            font-size: 11px;
            font-weight: 900;
            letter-spacing: 1.5px;
            text-shadow: 0 0 10px rgba(255,120,0,0.7), 1px 1px 2px rgba(0,0,0,0.9);
        }

        #bm-modal-close {
            background: none;
            border: 1px solid #5a1a00;
            color: #b85a20;
            font-family: 'Arial Black', Arial, sans-serif;
            font-size: 12px;
            font-weight: 900;
            width: 22px;
            height: 22px;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 0;
            line-height: 1;
            transition: all 0.12s ease;
        }

        #bm-modal-close:hover {
            background: rgba(150,30,0,0.5);
            border-color: #aa4500;
            color: #ff6020;
        }

        #bm-modal-iframe {
            flex: 1;
            width: 100%;
            border: none;
            display: block;
            background: #0d0500;
        }

        /* ==================== BOSS TIMER ==================== */
        #bt-body {
            display: flex;
            flex-direction: column;
            gap: 6px;
        }

        /* Wrapper for the two buttons (Boss Map + Timer) */
        #bt-buttons {
            display: flex;
            align-items: stretch;
            gap: 6px;
        }

        #bt-boss-btn {
            flex: 1;
            color: #af9b6d;
            text-shadow: 0 0 8px rgba(175,155,109,0.5), 1px 1px 2px rgba(0,0,0,0.9);
        }

        #bt-timer {
            flex: 1;
            font-size: 11px;
            letter-spacing: 2px;
            min-width: 58px;
        }

        /* Devil Hound alert banner — appears below the buttons */
        #bt-devil-banner {
            display: flex;
            align-items: center;
            gap: 6px;
            padding: 6px 0 0 0;
            background: transparent;
            animation: bt-banner-in 0.3s ease;
        }

        @keyframes bt-banner-in {
            from { opacity: 0; transform: translateY(-4px); }
            to   { opacity: 1; transform: translateY(0); }
        }

        #bt-devil-dot {
            width: 7px;
            height: 7px;
            border-radius: 50%;
            background: #e87030;
            flex-shrink: 0;
            animation: bt-dot-blink 1.4s ease-in-out infinite;
        }

        @keyframes bt-dot-blink {
            0%, 100% { opacity: 1; box-shadow: 0 0 4px rgba(232,112,48,0.8); }
            50%       { opacity: 0.3; box-shadow: none; }
        }

        /* Pulsing glow on the Devil Hound text when active */
        #bt-devil-text {
            color: #e87030;
            font-family: 'Arial Black', 'Arial Bold', Arial, sans-serif;
            font-size: 9px;
            font-weight: 900;
            letter-spacing: 0.8px;
            text-shadow: 0 0 6px rgba(255,120,0,0.5), 1px 1px 2px rgba(0,0,0,0.9);
            animation: bt-devil-text-pulse 1.4s ease-in-out infinite;
        }

        @keyframes bt-devil-text-pulse {
            0%, 100% { text-shadow: 0 0 6px rgba(255,120,0,0.5), 1px 1px 2px rgba(0,0,0,0.9); }
            50%       { text-shadow: 0 0 12px rgba(255,120,0,0.85), 1px 1px 2px rgba(0,0,0,0.9); }
        }

        #bt-timer.bt-urgent {
            color: #cc3030;
            border-color: #6a1515;
            border-top-color: #8a2020;
            text-shadow: 0 0 12px rgba(255,50,50,0.8), 1px 1px 2px rgba(0,0,0,0.9);
            animation: bt-pulse 1s ease-in-out infinite;
        }

        @keyframes bt-pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.5; }
        }

        /* ==================== AUTOBANK ==================== */
        #ab-buttons {
            display: flex;
            flex-direction: column;
            gap: 4px;
        }

        #ab-buttons .df-btn { width: 100%; }

        .df-btn.df-btn-deposit {
            color: #8a1a1a;
            border-color: #3a0a0a;
            border-top-color: #5a1515;
        }

        .df-btn.df-btn-deposit:hover {
            background: linear-gradient(180deg, #2a0a0a 0%, #1a0606 50%, #200808 100%);
            border-color: #6a1515;
            border-top-color: #8a2020;
            color: #cc3030;
            text-shadow: 0 0 12px rgba(255,50,50,0.7), 1px 1px 2px rgba(0,0,0,0.9);
        }

        /* ==================== QUICK SERVICE ==================== */
        #qs-buttons {
            display: flex;
            flex-direction: column;
            gap: 4px;
        }

        #qs-buttons .df-btn { width: 100%; }

        /* 3-column grid for the three service buttons */
        #qs-service-grid {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 4px;
        }

        #qs-service-grid .df-btn { width: 100%; }

        .df-btn.df-btn-all-services {
            color: #8a1a1a;
            border-color: #3a0a0a;
            border-top-color: #5a1515;
            width: 100%;
        }

        .df-btn.df-btn-all-services:hover {
            background: linear-gradient(180deg, #2a0a0a 0%, #1a0606 50%, #200808 100%);
            border-color: #6a1515;
            border-top-color: #8a2020;
            color: #cc3030;
            text-shadow: 0 0 12px rgba(255,50,50,0.7), 1px 1px 2px rgba(0,0,0,0.9);
        }

        /* ==================== QUICKBUY ==================== */
        #qb-body .df-btn {
            font-size: 8px;
            letter-spacing: 0.3px;
            width: 100%;
        }

        .qb-section-title {
            color: #e87030;
            font-size: 9px;
            font-weight: 900;
            letter-spacing: 1px;
            text-shadow: 0 0 8px rgba(200,80,0,0.4);
            margin: 4px 0 6px;
            padding-bottom: 4px;
            border-bottom: 1px solid #3a1000;
        }

        .qb-grid {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 4px;
            margin-bottom: 4px;
        }

        .df-btn.qb-highlighted {
            background: linear-gradient(180deg, #0a2a00 0%, #051800 50%, #081e00 100%);
            border-color: #2a5a00;
            border-top-color: #3a7a00;
            color: #60cc20;
            text-shadow: 0 0 8px rgba(80,200,0,0.5), 1px 1px 2px rgba(0,0,0,0.9);
        }

        .df-btn.qb-highlighted:hover {
            background: linear-gradient(180deg, #0f3a00 0%, #082200 50%, #0c2800 100%);
            border-color: #3a7a00;
            color: #80ee40;
            text-shadow: 0 0 12px rgba(100,255,0,0.7), 1px 1px 2px rgba(0,0,0,0.9);
        }
    `;

    var styleEl = document.createElement('style');
    styleEl.textContent = css;
    document.head.appendChild(styleEl);

    ///////////////////////////////////////////
    //  Panel Helpers                        //
    ///////////////////////////////////////////

    function makePanel(id) {
        var panel = document.createElement('div');
        panel.id = id + '-panel';
        panel.className = 'df-panel';
        var inner = document.createElement('div');
        inner.className = 'df-panel-inner';
        panel.appendChild(inner);
        return { panel, inner };
    }

    function makeHeader(inner, titleText, collapseKey, bodyEl) {
        var header = document.createElement('div');
        header.className = 'df-panel-header';

        var title = document.createElement('span');
        title.className = 'df-panel-title';
        title.textContent = titleText;

        var btn = document.createElement('button');
        btn.className = 'df-panel-collapse';
        btn.textContent = '–';

        header.appendChild(title);
        header.appendChild(btn);
        inner.appendChild(header);

        // Divider line — real HTML element, not a CSS border (immune to sub-pixel zoom artifacts)
        var divider = document.createElement('div');
        divider.className = 'df-panel-divider';
        inner.appendChild(divider);

        var isCollapsed = localStorage.getItem(collapseKey) === 'true';
        if(isCollapsed) {
            bodyEl.style.display = 'none';
            divider.style.display = 'none';
            btn.textContent = '+';
        }

        btn.addEventListener('click', function() {
            isCollapsed = !isCollapsed;
            bodyEl.style.display = isCollapsed ? 'none' : '';
            divider.style.display = isCollapsed ? 'none' : '';
            btn.textContent = isCollapsed ? '+' : '–';
            localStorage.setItem(collapseKey, isCollapsed ? 'true' : 'false');
        });
    }

    function makeButton(label, extraClass) {
        var btn = document.createElement('button');
        btn.className = 'df-btn' + (extraClass ? ' ' + extraClass : '');
        btn.textContent = label;
        return btn;
    }

    function makeSeparator() {
        var sep = document.createElement('div');
        sep.className = 'df-separator';
        return sep;
    }

    ///////////////////////////////////////////
    //  Boss Timer Panel                     //
    ///////////////////////////////////////////

    function openBossMapModal() {
        if(document.getElementById('bm-overlay')) return;

        var overlay = document.createElement('div');
        overlay.id = 'bm-overlay';

        var modal = document.createElement('div');
        modal.id = 'bm-modal';

        var header = document.createElement('div');
        header.id = 'bm-modal-header';

        var titleEl = document.createElement('span');
        titleEl.id = 'bm-modal-title';
        titleEl.textContent = 'BOSS MAP';

        var closeBtn = document.createElement('button');
        closeBtn.id = 'bm-modal-close';
        closeBtn.textContent = '✕';
        closeBtn.addEventListener('click', function() {
            document.body.removeChild(overlay);
        });

        header.appendChild(titleEl);
        header.appendChild(closeBtn);

        var divider = document.createElement('div');
        divider.id = 'bm-modal-divider';

        var iframe = document.createElement('iframe');
        iframe.id = 'bm-modal-iframe';
        iframe.src = 'https://www.dfprofiler.com/bossmap';
        iframe.setAttribute('allowfullscreen', '');

        modal.appendChild(header);
        modal.appendChild(divider);
        modal.appendChild(iframe);
        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        // Close when clicking the dark overlay outside the modal
        overlay.addEventListener('click', function(e) {
            if(e.target === overlay) document.body.removeChild(overlay);
        });

        // Close with ESC key
        function onKeyDown(e) {
            if(e.key === 'Escape') {
                if(document.getElementById('bm-overlay')) document.body.removeChild(overlay);
                document.removeEventListener('keydown', onKeyDown);
            }
        }
        document.addEventListener('keydown', onKeyDown);
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // DEVIL HOUND SPECIAL SPAWN TRACKER
    // ═══════════════════════════════════════════════════════════════════════════
    // Polls dfprofiler.com/bossmap/json every 2 minutes to detect special Devil Hound spawns.
    // Only detects the INNER CITY special spawn (boss_num < 10), not regular Wasteland spawns.
    // Shows alert banner below Boss Timer when detected.                  //
    ///////////////////////////////////////////

    // Calls the dfprofiler bossmap JSON endpoint and checks if Devil Hound is active.
    //
    // JSON shape: object with numeric string keys ("0", "1", ...) plus "bosshash" and "servertime".
    // Each entry has a "special_enemy_type" field like "1 x Devil Hound" or "2 x Flaming Titan".
    // Ghost hounds are entries where "reward_cash" and "reward_exp" are both "0",
    // "event_type" is "" and the entry has no dfp_objectives — same as regular boss slots.
    // The dfprofiler site distinguishes ghosts via isoa/location being off-map, which we
    // cannot easily check — but since the user only wants to know if a real DH is up,
    // detecting ANY "Devil Hound" in special_enemy_type is sufficient (ghost or not, it's still up).
    // Returns a Promise<bool>: true if at least one Devil Hound entry exists, false otherwise.
    function checkDevilHound() {
        return new Promise(function(resolve) {
            GM.xmlHttpRequest({
                method: 'GET',
                url: 'https://www.dfprofiler.com/bossmap/json/?_=' + Date.now(),
                headers: {
                    'Accept': 'application/json, text/javascript, */*',
                    'X-Requested-With': 'XMLHttpRequest'
                },
                onload: function(response) {
                    try {
                        var data = JSON.parse(response.responseText);
                        var found = false;

                        // Iterate over numeric-keyed entries — skip meta keys like "bosshash" / "servertime"
                        for(var key in data) {
                            if(!data.hasOwnProperty(key)) continue;
                            if(isNaN(parseInt(key))) continue; // skip non-numeric keys
                            var entry = data[key];
                            var enemyType = (entry.special_enemy_type || '').toLowerCase();
                            var bossNum = parseInt(entry.boss_num || '999');
                            
                            // Only detect the SPECIAL daily Devil Hound spawn (Inner City, near Secronom).
                            // The special spawn has a low boss_num (1-6 range), while the regular Wasteland
                            // Devil Hound spawns have boss_num 27+. Filter to boss_num < 10 to catch only special.
                            if(enemyType.includes('devil hound') && bossNum < 10) {
                                found = true;
                            }
                        }
                        
                        resolve(found);
                    } catch(e) {
                        resolve(false);
                    }
                },
                onerror: function() { 
                    resolve(false); 
                }
            });
        });
    }

        // Starts polling dfprofiler every 2 minutes to track Devil Hound status.
    // Updates the banner and button pulse state accordingly.
    function startDevilHoundTracker(bossBtn, btInner) {
        var bannerEl = null;

        function setBannerVisible(visible) {
            if(visible && !bannerEl) {
                // Create the banner and insert it INSIDE bt-body, after the buttons
                bannerEl = document.createElement('div');
                bannerEl.id = 'bt-devil-banner';

                var dot = document.createElement('div');
                dot.id = 'bt-devil-dot';

                var text = document.createElement('span');
                text.id = 'bt-devil-text';
                text.textContent = 'DEVIL HOUND HAS SPAWNED';

                bannerEl.appendChild(dot);
                bannerEl.appendChild(text);

                // Insert banner as the last child of bt-body (after boss map button and timer)
                var btBody = document.getElementById('bt-body');
                if(btBody) {
                    btBody.appendChild(bannerEl);
                }

            } else if(!visible && bannerEl) {
                bannerEl.remove();
                bannerEl = null;
            }
        }

        async function poll() {
            var active = await checkDevilHound();
            setBannerVisible(active);
        }

        // First check after 1 second (to ensure DOM is ready), then every 2 minutes
        setTimeout(poll, 1000);
        setInterval(poll, 2 * 60 * 1000);
    }

    function getSecondsUntilNextHour() {
        var now = new Date();
        return (59 - now.getMinutes()) * 60 + (59 - now.getSeconds());
    }

    function formatTime(totalSeconds) {
        var m = Math.floor(totalSeconds / 60);
        var s = totalSeconds % 60;
        return (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;
    }

    function buildBossPanel() {
        var body = document.createElement('div');
        body.id = 'bt-body';
        body.className = 'df-panel-body';

        var built = makePanel('bt');
        makeHeader(built.inner, 'BOSS TIMER', 'btCollapsed', body);
        built.inner.appendChild(body);

        // Create wrapper for the two buttons
        var buttonsWrapper = document.createElement('div');
        buttonsWrapper.id = 'bt-buttons';

        var bossBtn = makeButton('BOSS MAP', '');
        bossBtn.id = 'bt-boss-btn';
        bossBtn.addEventListener('click', function() { openBossMapModal(); });

        var timerEl = document.createElement('div');
        timerEl.id = 'bt-timer';
        timerEl.className = 'df-btn';
        timerEl.textContent = formatTime(getSecondsUntilNextHour());

        buttonsWrapper.appendChild(bossBtn);
        buttonsWrapper.appendChild(timerEl);
        body.appendChild(buttonsWrapper);

        setInterval(function() {
            var remaining = getSecondsUntilNextHour();
            timerEl.textContent = formatTime(remaining);
            if(remaining <= 300) timerEl.classList.add('bt-urgent');
            else timerEl.classList.remove('bt-urgent');
        }, 1000);

        // Start Devil Hound tracker on all pages (1 second delay ensures DOM is ready)
        startDevilHoundTracker(bossBtn, built.inner);

        return built.panel;
    }

    ///////////////////////////////////////////
    //  Autobank Panel                       //
    ///////////////////////////////////////////

    function buildAutobankPanel() {
        var buttonsDiv = document.createElement('div');
        buttonsDiv.id = 'ab-buttons';

        var body = document.createElement('div');
        body.className = 'df-panel-body';
        body.appendChild(buttonsDiv);

        var built = makePanel('ab');
        makeHeader(built.inner, 'AUTO BANK', 'ab2Collapsed', body);
        built.inner.appendChild(body);

        var buttonDefs = [
            { label: 'WITHDRAW 50K', action: 'withdraw', amount: '50000', deposit: false },
            { label: 'WITHDRAW 150K', action: 'withdraw', amount: '150000', deposit: false },
            { label: 'WITHDRAW 5M', action: 'withdraw', amount: '5000000', deposit: false },
            { label: 'WITHDRAW ALL', action: 'withdrawAll', amount: null, deposit: false },
            { separator: true },
            { label: 'DEPOSIT ALL', action: 'deposit', amount: null, deposit: true },
        ];

        buttonDefs.forEach(function(def) {
            if(def.separator) { buttonsDiv.appendChild(makeSeparator()); return; }
            var btn = makeButton(def.label, def.deposit ? 'df-btn-deposit' : '');
            btn.addEventListener('click', function() {
                var si = document.getElementById('searchField') ||
                    document.querySelector("input[name='searchField']") ||
                    document.querySelector("input[type='text'][name='item_name']");
                if(si) localStorage.setItem('lastDFsearch', si.value);
                var url = `${origin}${path}?page=15&scripts=${def.action}`;
                if(def.amount) url += `&amount=${def.amount}`;
                if(currentPage) url += `&originPage=${currentPage}`;
                window.location.replace(url);
            });
            buttonsDiv.appendChild(btn);
        });

        return built.panel;
    }

    ///////////////////////////////////////////
    //  Quick Service Panel                  //
    ///////////////////////////////////////////

    function buildQuickServicePanel() {
        var buttonsDiv = document.createElement('div');
        buttonsDiv.id = 'qs-buttons';

        var body = document.createElement('div');
        body.className = 'df-panel-body';
        body.appendChild(buttonsDiv);

        var built = makePanel('qs');
        makeHeader(built.inner, 'QUICK SERVICE', 'qsCollapsed', body);
        built.inner.appendChild(body);

        var cookBtn = makeButton('COOKING', '');
        cookBtn.addEventListener('click', function() { qsDoFood(false); });

        var medBtn = makeButton('MEDICAL', '');
        medBtn.addEventListener('click', function() { qsDoMedical(false); });

        var repairBtn = makeButton('REPAIR', '');
        repairBtn.addEventListener('click', function() { qsDoRepair(false); });

        // 3-column grid for the three service buttons
        var serviceGrid = document.createElement('div');
        serviceGrid.id = 'qs-service-grid';
        serviceGrid.appendChild(cookBtn);
        serviceGrid.appendChild(medBtn);
        serviceGrid.appendChild(repairBtn);
        buttonsDiv.appendChild(serviceGrid);

        // Separator + ALL SERVICES inside buttonsDiv — matches Autobank layout
        buttonsDiv.appendChild(makeSeparator());
        var allBtn = makeButton('ALL SERVICES', 'df-btn-all-services');
        allBtn.addEventListener('click', function() { qsDoAll(); });
        buttonsDiv.appendChild(allBtn);

        return built.panel;
    }

    ///////////////////////////////////////////
    //  QuickBuy Panel                       //
    ///////////////////////////////////////////

    const highlightConfig = JSON.parse(localStorage.getItem('qb_highlightConfig') || '{}');

    const foodMedItems = [
        { label: 'Buy Whiskey', search: 'Whiskey', qty: 1, count: 1 },
        { label: 'Buy Nerotonin 8B', search: 'Nerotonin 8B', qty: 1, count: 1 },
        { label: 'Buy Energy Bar', search: 'Energy Bar', qty: 1, count: 1 },
        { label: 'Buy Repair Kit', search: 'Repair Kit', qty: 1, count: 1 }
    ];

    const ammoItems = [
        { label: '14mm Stack (1200)', search: '14mm Rifle Bullets', qty: 1200, count: 1 },
        { label: '12.7mm Stack (1200)', search: '12.7mm Rifle Bullets', qty: 1200, count: 1 },
        { label: '9mm Stack (1200)', search: '9mm Rifle Bullets', qty: 1200, count: 1 },
        { label: '.55 Stack (1600)', search: '.55 Handgun Bullets', qty: 1600, count: 1 },
        { label: 'Biomass Stack (1000)', search: 'Biomass', qty: 1000, count: 1 },
        { label: 'Energy Cell (1600)', search: 'Energy Cell', qty: 1600, count: 1 },
        { label: 'Grenade Stack (400)', search: 'Grenades', qty: 400, count: 1 },
        { label: 'Heavy Grenades (400)', search: 'Heavy Grenades', qty: 400, count: 1 },
        { label: 'Gasoline (4546)', search: 'Gasoline', qty: 4546, count: 1 },
        { label: '10 Gauge (800)', search: '10 Gauge Shells', qty: 800, count: 1 },
        { label: '12 Gauge (800)', search: '12 Gauge Shells', qty: 800, count: 1 },
        { label: '16 Gauge (800)', search: '16 Gauge Shells', qty: 800, count: 1 },
        { label: '20 Gauge (800)', search: '20 Gauge Shells', qty: 800, count: 1 }
    ];

    function buildQBSection(items) {
        var grid = document.createElement('div');
        grid.className = 'qb-grid';
        items.forEach(function(item) {
            var btn = makeButton(item.label, highlightConfig[item.search] ? 'qb-highlighted' : '');
            btn.addEventListener('contextmenu', function(e) {
                e.preventDefault();
                if(highlightConfig[item.search]) {
                    delete highlightConfig[item.search];
                    btn.classList.remove('qb-highlighted');
                } else {
                    highlightConfig[item.search] = true;
                    btn.classList.add('qb-highlighted');
                }
                localStorage.setItem('qb_highlightConfig', JSON.stringify(highlightConfig));
            });
            btn.addEventListener('click', function() {
                // Check if the player has enough cash before navigating to the marketplace
                var userVars = getUserVars();
                if(userVars && parseInt(userVars.DFSTATS_df_cash) <= 0) {
                    showMoneyWarning();
                    return;
                }
                sessionStorage.setItem('quickBuy_pending', JSON.stringify({ term: item.search, qty: item.qty, count: item.count }));
                window.location.href = origin + path + '?page=35';
            });
            grid.appendChild(btn);
        });
        return grid;
    }

    function buildQuickBuyPanel() {
        var body = document.createElement('div');
        body.id = 'qb-body';
        body.className = 'df-panel-body';

        var built = makePanel('qb');
        makeHeader(built.inner, 'QUICK BUY', 'quickbuyCollapsed', body);
        built.inner.appendChild(body);

        var foodTitle = document.createElement('div');
        foodTitle.className = 'qb-section-title';
        foodTitle.textContent = 'FOOD / MEDICAL';
        body.appendChild(foodTitle);
        body.appendChild(buildQBSection(foodMedItems));
        body.appendChild(makeSeparator());

        var ammoTitle = document.createElement('div');
        ammoTitle.className = 'qb-section-title';
        ammoTitle.textContent = 'AMMO';
        body.appendChild(ammoTitle);
        body.appendChild(buildQBSection(ammoItems));

        return built.panel;
    }

    ///////////////////////////////////////////
    //  QuickBuy Purchase Logic              //
    ///////////////////////////////////////////

    function realClick(el) {
        el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
    }

    function waitForYesClick(cb) {
        var start = Date.now();
        (function poll() {
            var yes = Array.from(document.querySelectorAll('button'))
                .find(function(b) { return b.innerText.trim().toLowerCase() === 'yes'; });
            if(yes) { realClick(yes); if(cb) cb(); }
            else if(Date.now() - start < 5000) setTimeout(poll, 100);
        })();
    }

    function purchaseMultiple(term, qty, count) {
        var obs = new MutationObserver(function(_, o) {
            var items = Array.from(document.querySelectorAll('div.fakeItem'))
                .filter(function(d) {
                    return d.querySelector('.itemName')?.textContent.trim() === term &&
                        Number(d.getAttribute('data-quantity')) === qty;
                });
            if(items.length) {
                o.disconnect();
                var buyBtn = items[0].querySelector('button[data-action="buyItem"]');
                var i = 0;
                (function loop() {
                    if(i >= count) return;
                    realClick(buyBtn);
                    waitForYesClick(function() { i++; setTimeout(loop, 300); });
                })();
            }
        });
        obs.observe(document.body, { childList: true, subtree: true });
    }

    ///////////////////////////////////////////
    //  Layout & Positioning                 //
    ///////////////////////////////////////////

    function getRightColumnCell() {
        return document.querySelector("td.design2010[style*='right_margin.jpg']");
    }

    // Places the Boss Timer in a fixed position for pages that lack the right column
    function positionBossTimerFixed(btPanel) {
        btPanel.style.position = 'fixed';
        btPanel.style.top = '120px';
        btPanel.style.right = '10px';
        btPanel.style.width = '280px';
        btPanel.style.zIndex = '99999';
        document.body.appendChild(btPanel);
    }

    function positionPanels(btPanel, abPanel, qsPanel, qbPanel) {
        var rightTd = getRightColumnCell();
        if(!rightTd) return false;

        rightTd.style.position = 'relative';

        [btPanel, abPanel, qsPanel, qbPanel].forEach(function(p) {
            if(!p.parentElement || p.parentElement !== rightTd) rightTd.appendChild(p);
            p.style.position = 'absolute';
            p.style.left = '10px';
        });

        function reflow() {
            var tdWidth = rightTd.offsetWidth;
            var width = (tdWidth > 60 ? tdWidth - 20 : 420) + 'px';
            [btPanel, abPanel, qsPanel, qbPanel].forEach(function(p) { p.style.width = width; });

            var GAP = 8;
            var top = 120;
            btPanel.style.top = top + 'px'; top += btPanel.offsetHeight + GAP;
            abPanel.style.top = top + 'px'; top += abPanel.offsetHeight + GAP;
            qsPanel.style.top = top + 'px'; top += qsPanel.offsetHeight + GAP;
            qbPanel.style.top = top + 'px';
        }

        reflow();

        var ro = new ResizeObserver(reflow);
        [btPanel, abPanel, qsPanel, qbPanel, rightTd].forEach(function(el) { ro.observe(el); });
        if(window.visualViewport) window.visualViewport.addEventListener('resize', reflow);

        return true;
    }

    ///////////////////////////////////////////
    //  Init                                 //
    ///////////////////////////////////////////

    async function init() {
        // Special pages: show only the Boss Timer panel
        if(isBossTimerOnly) {
            var btPanelSpecial = buildBossPanel();

            function placeBT() {
                var rightTd = getRightColumnCell();
                if(rightTd) {
                    rightTd.style.position = 'relative';
                    rightTd.appendChild(btPanelSpecial);
                    btPanelSpecial.style.position = 'absolute';
                    btPanelSpecial.style.left = '10px';
                    btPanelSpecial.style.top = '120px';

                    function reflow() {
                        var w = rightTd.offsetWidth;
                        btPanelSpecial.style.width = (w > 60 ? w - 20 : 280) + 'px';
                    }
                    reflow();
                    var ro = new ResizeObserver(reflow);
                    ro.observe(rightTd);
                    if(window.visualViewport) window.visualViewport.addEventListener('resize', reflow);
                } else {
                    // No right column found — fall back to a fixed position
                    positionBossTimerFixed(btPanelSpecial);
                }
            }

            if(document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', placeBT);
            } else {
                placeBT();
            }

            console.log('DF Panels 2.0.1 - Boss Timer only');
            return;
        }

        // Normal pages: all four panels
        helpWindow = unsafeWindow.df_prompt;
        initUserData();
        await loadSavedMarketData();
        refreshServicesDataBank();
        window.addEventListener("beforeunload", saveMarketData);
        checkPendingAction();

        var btPanel = buildBossPanel();
        var abPanel = buildAutobankPanel();
        var qsPanel = buildQuickServicePanel();
        var qbPanel = buildQuickBuyPanel();

        function tryPlace(attempt) {
            attempt = attempt || 0;
            var ok = positionPanels(btPanel, abPanel, qsPanel, qbPanel);
            if(!ok && attempt < 20) setTimeout(function() { tryPlace(attempt + 1); }, 200);
        }
        setTimeout(tryPlace, 500);

        // QuickBuy: execute any pending purchase from a previous page navigation
        var p = sessionStorage.getItem('quickBuy_pending');
        if(p && window.location.search.includes('page=35')) {
            var pending = JSON.parse(p);
            sessionStorage.removeItem('quickBuy_pending');
            var input = document.querySelector('#searchField');
            var mk = document.querySelector('#makeSearch');
            if(input && mk) {
                input.value = pending.term;
                realClick(mk);
                setTimeout(function() { purchaseMultiple(pending.term, pending.qty, pending.count); }, 500);
            }
        }

        console.log("DF Panels 2.0.1 - Loaded successfully!");
    }

    // Special pages: run on DOMContentLoaded so the panel appears immediately
    // Normal pages: run on load so the game has time to initialize unsafeWindow variables
    if(isBossTimerOnly) {
        if(document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
        } else {
            init();
        }
    } else {
        window.addEventListener('load', init);
    }

})();