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);
    }

})();