MOOMOO.IO Utility Mod! (Scrollable Inventory, Wearables Hotbar, Typing Indicator, & More!)

Enhances MooMoo.io with mini-mods to level the playing field against cheaters whilst being fair to non-script users.

// ==UserScript==
// @name         MOOMOO.IO Utility Mod! (Scrollable Inventory, Wearables Hotbar, Typing Indicator, & More!)
// @namespace    https://greatest.deepsurf.us/users/137913
// @description  Enhances MooMoo.io with mini-mods to level the playing field against cheaters whilst being fair to non-script users.
// @license      GNU GPLv3 with the condition: no auto-heal or instant kill features may be added to the licensed material.
// @author       TigerYT
// @version      1.0.1
// @grant        GM_info
// @match        *://moomoo.io/*
// @match        *://dev.moomoo.io/*
// @match        *://sandbox.moomoo.io/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=moomoo.io
// @run-at       document-start
// ==/UserScript==

/*
Version numbers: A.B.C
A = Added or made a major change to multiple mini-mods
B = Added or made a major change to a feature (a whole mini-mod, or major parts within a mini-mod)
C = Added patches
*/

(function() {
    'use strict';

    /**
     * Asynchronously retrieves the userscript's metadata object.
     * This is the recommended, universally compatible method.
     *
     * @returns {Promise<object>} A Promise that resolves with the script info object.
     */
    const getGMInfo = async () => {
        // Modern API (Greasemonkey 4+)
        if (typeof GM !== 'undefined' && typeof GM.info === 'function') {
            return await GM.info;
        }

        // Legacy API (Tampermonkey, Violentmonkey, etc.)
        if (typeof GM_info !== 'undefined') {
            return GM_info;
        }

        // If neither is found, reject the promise
        throw new Error("getGMInfoAsync() Error: Userscript manager info object not found. Make sure you have '@grant GM_info' in your script's header.");
    }

    /**
     * @module Logger
     * @description A simple, configurable logger to prefix messages and avoid cluttering the console.
     * It respects the `DEBUG_MODE` flag in the main module's config.
     */
    const Logger = {
        /**
         * Logs a standard message to the console if DEBUG_MODE is enabled.
         * @param {string} message - The primary message to log.
         * @param {...any} args - Additional arguments to pass directly to console.log.
         */
        log: (message, ...args) => MooMooUtilityMod.config.DEBUG_MODE && console.log((args[0] && typeof args[0] == "string" && args[0].startsWith('color') ? '%c' : '') + `[Util-Mod] ${message}`, ...args),
        /**
         * Logs an informational message to the console if DEBUG_MODE is enabled.
         * @param {string} message - The primary message to log.
         * @param {...any} args - Additional arguments to pass directly to console.info.
         */
        info: (message, ...args) => MooMooUtilityMod.config.DEBUG_MODE && console.info((args[0] && typeof args[0] == "string" && args[0].startsWith('color') ? '%c' : '') + `[Util-Mod] ${message}`, ...args),
        /**
         * Logs a warning message to the console if DEBUG_MODE is enabled.
         * @param {string} message - The primary message to log.
         * @param {...any} args - Additional arguments to pass directly to console.warn.
         */
        warn: (message, ...args) => MooMooUtilityMod.config.DEBUG_MODE && console.warn((args[0] && typeof args[0] == "string" && args[0].startsWith('color') ? '%c' : '') + `[Util-Mod] ${message}`, ...args),
        /**
         * Logs an error message. This is always shown, regardless of the DEBUG_MODE setting.
         * @param {string} message - The primary message to log.
         * @param {...any} args - Additional arguments to pass directly to console.error.
         */
        error: (message, ...args) => console.error((args[0] && typeof args[0] == "string" && args[0].startsWith('color') ? '%c' : '') + `[Util-Mod] ${message}`, ...args)
    };

    /**
      * @module MooMooUtilityMod
      * @description The core of the utility mod. It manages shared state, data, network hooks,
      * and initializes all registered "minimods".
      */
    const MooMooUtilityMod = {

        // --- CORE MOD PROPERTIES ---

        /**
         * @property {object} config - Holds user-configurable settings for the script.
         */
        config: {
            /** @property {boolean} DEBUG_MODE - Set to true to see detailed logs in the console. */
            DEBUG_MODE: true
        },

        /**
         * @property {object} state - Holds the dynamic state of the script, changing as the user plays.
         */
        state: {
            /** @property {boolean} enabled - Master toggle for the entire utility mod. Set to false to disable all features. */
            enabled: true,

            /** @property {number} initTimestamp - The UNIX timestamp (in milliseconds) when the script was initiated. */
            initTimestamp: Date.now(),

            /** @property {boolean} codecsReady - Tracks if the msgpack encoder and decoder instances have been successfully captured. */
            codecsReady: false,

            /** @property {boolean} socketReady - Tracks if the game's WebSocket instance has been successfully captured. */
            socketReady: false,

            /** @property {boolean} isSandbox - Tracks if the player is in sandbox mode, which affects item placement limits. */
            isSandbox: window.location.host.startsWith('sandbox'),

            /** @property {WebSocket|null} gameSocket - A direct reference to the game's main WebSocket instance. */
            gameSocket: null,

            /** @property {object|null} gameEncoder - A direct reference to the game's msgpack encoder instance. */
            gameEncoder: null,

            /** @property {object|null} gameDecoder - A direct reference to the game's msgpack decoder instance. */
            gameDecoder: null,

            /** @property {number} playerId - The client player's unique server-side ID, assigned upon joining a game. */
            playerId: -1,

            /** @property {{food: number, wood: number, stone: number, gold: number}} playerResources - The player's current resource counts. */
            playerResources: { food: 0, wood: 0, stone: 0, gold: 0 },

            /** @property {Map<number, number>} playerPlacedItemCounts - Maps an item's limit group ID to the number of items placed from that group. */
            playerPlacedItemCounts: new Map(),

            /** @property {boolean} playerHasRespawned - Tracks if the player has died and respawned, used to manage certain UI elements. */
            playerHasRespawned: false,

            /** @property {Array<MutationObserver|ResizeObserver>} observers - Stores all active observers for easy disconnection and cleanup when the mod is disabled. */
            observers: [],

            /** @property {Array<string>} focusableElementIds - A list of DOM element IDs that should block hotkeys when visible. Minimods can add to this list. */
            focusableElementIds: [],
        },

        /**
         * @property {object} data - Contains structured, static data about the game, such as items and packet definitions.
         */
        data: {
            /** @property {Map<number, object>} _itemDataByServerId - A map for quickly looking up item data by its server-side ID. */
            _itemDataByServerId: new Map(),

            /** @property {Map<number, object[]>} _itemDataBySlot - A map for grouping items by their action bar slot (e.g., Food, Walls, Spikes). */
            _itemDataBySlot: new Map(),

            /**
             * @property {object} constants - A collection of named constants to avoid "magic values" in the code.
             * These are "universal" constants that multiple minimods may need access to.
             */
            constants: {
                PACKET_TYPES: {
                    USE_ITEM: 'F',
                    EQUIP_ITEM: 'z',
                    EQUIP_WEARABLE: 'c',
                    CHAT: '6'
                },
                PACKET_DATA: {
                    WEARABLE_TYPES: {
                        HAT: 'hat',
                        ACCESSORY: 'accessory',
                    },
                    STORE_ACTIONS: {
                        ADD_ITEM: 'buy',
                        UPDATE_EQUIPPED: 'equip',
                    },
                    USE_ACTIONS: {
                        START_USING: 1,
                        STOP_USING: 0,
                    }
                },
                ITEM_TYPES: {
                    PRIMARY_WEAPON: 0,
                    SECONDARY_WEAPON: 1,
                    FOOD: 2,
                    WALL: 3,
                    SPIKE: 4,
                    WINDMILL: 5,
                    FARM: 6,
                    TRAP: 7,
                    EXTRA: 8,
                    SPAWN_PAD: 9
                },
                DOM: {
                    // IDs
                    UTILITY_MOD_STYLES: 'utilityModStyles',
                    UTILITY_MOD_SCRIPTS: 'utilityModScripts',
                    MENU_CONTAINER: 'menuContainer',
                    MAIN_MENU: 'mainMenu',
                    STORE_MENU: 'storeMenu',
                    STORE_HOLDER: 'storeHolder',
                    RESOURCE_DISPLAY: 'resDisplay',
                    CHAT_HOLDER: 'chatHolder',
                    CHAT_BOX: 'chatBox',
                    ALLIANCE_MENU: 'allianceMenu',
                    ACTION_BAR: 'actionBar',
                    GAME_CANVAS: 'gameCanvas',
                    GAME_UI: 'gameUI',
                    DIED_TEXT: 'diedText',
                    ENTER_GAME_BUTTON: 'enterGame',
                    UPGRADE_HOLDER: 'upgradeHolder',
                    UPGRADE_COUNTER: 'upgradeCounter',
                    ITEM_INFO_HOLDER: 'itemInfoHolder',
                    GAME_TITLE: 'gameName',
                    LOADING_TEXT: 'loadingText',
                    LOADING_INFO: 'loadingInfo',
                    AD_HOLDER: 'promoImgHolder',
                    WIDE_AD_CARD: 'wideAdCard',
                    AD_CARD: 'adCard',
                    RIGHT_CARD_HOLDER: 'rightCardHolder',
                    MENU_CARD_HOLDER: 'menuCardHolder',
                    SHUTDOWN_DISPLAY: 'shutdownDisplay',
                    LINKS_CONTAINER: 'linksContainer2',

                    // Selectors / Patterns / Classes
                    ACTION_BAR_ITEM_REGEX: /^actionBarItem(\d+)$/,
                    ACTION_BAR_ITEM_CLASS: '.actionBarItem',
                    STORE_MENU_EXPANDED_CLASS: 'expanded',
                    MENU_CARD_CLASS: 'menuCard',
                    STORE_TAB_CLASS: 'storeTab',
                    MENU_LINK_CLASS: 'menuLink',
                    PASSTHROUGH_CLASS: 'passthrough',
                },
                CSS: {
                    DISPLAY_NONE: 'none',
                    DISPLAY_FLEX: 'flex',
                    DISPLAY_BLOCK: 'block',
                    OPAQUE: 1,
                },
                GAME_STATE: {
                    INITIAL_SELECTED_ITEM_INDEX: 0,
                    WEBSOCKET_STATE_OPEN: 1, // WebSocket.OPEN
                    NO_SCROLL: 0,
                    SCROLL_DOWN: 1,
                    SCROLL_UP: -1,
                },
                TIMEOUTS: {
                    MANUAL_CODEC_SCAN: 2500,
                },
            },

            /** @property {object} _rawItems - The raw item database, grouped by category for readability before processing. */
            _rawItems: {
                PRIMARY_WEAPONS: [
                    { id: 0, server_id: 0, name: "Tool Hammer" },
                    { id: 1, server_id: 1, name: "Hand Axe" },
                    { id: 3, server_id: 3, name: "Short Sword" },
                    { id: 4, server_id: 4, name: "Katana" },
                    { id: 5, server_id: 5, name: "Polearm" },
                    { id: 6, server_id: 6, name: "Bat" },
                    { id: 7, server_id: 7, name: "Daggers" },
                    { id: 8, server_id: 8, name: "Stick" },
                    { id: 2, server_id: 2, name: "Great Axe" },
                ],
                SECONDARY_WEAPONS: [
                    { id: 9, server_id: 9, name: "Hunting Bow", cost: { wood: 4 } },
                    { id: 10, server_id: 10, name: "Great Hammer" },
                    { id: 11, server_id: 11, name: "Wooden Shield" },
                    { id: 12, server_id: 12, name: "Crossbow", cost: { wood: 5 } },
                    { id: 13, server_id: 13, name: "Repeater Crossbow", cost: { wood: 10 } },
                    { id: 14, server_id: 14, name: "MC Grabby" },
                    { id: 15, server_id: 15, name: "Musket", cost: { stone: 10 } },
                ],
                FOOD: [
                    { id: 0, server_id: 16, name: "Apple", cost: { food: 10 } },
                    { id: 1, server_id: 17, name: "Cookie", cost: { food: 15 } },
                    { id: 2, server_id: 18, name: "Cheese", cost: { food: 25 } },
                ],
                WALLS: [
                    { id: 3, server_id: 19, name: "Wood Wall", limitGroup: 1, limit: 30, cost: { wood: 10 } },
                    { id: 4, server_id: 20, name: "Stone Wall", limitGroup: 1, limit: 30, cost: { stone: 25 } },
                    { id: 5, server_id: 21, name: "Castle Wall", limitGroup: 1, limit: 30, cost: { stone: 35 } },
                ],
                SPIKES: [
                    { id: 6, server_id: 22, name: "Spikes", limitGroup: 2, limit: 15, cost: { wood: 20, stone: 5 } },
                    { id: 7, server_id: 23, name: "Greater Spikes", limitGroup: 2, limit: 15, cost: { wood: 30, stone: 10 } },
                    { id: 8, server_id: 24, name: "Poison Spikes", limitGroup: 2, limit: 15, cost: { wood: 35, stone: 15 } },
                    { id: 9, server_id: 25, name: "Spinning Spikes", limitGroup: 2, limit: 15, cost: { wood: 30, stone: 20 } },
                ],
                WINDMILLS: [
                    { id: 10, server_id: 26, name: "Windmill", limitGroup: 3, limit: 7, sandboxLimit: 299, cost: { wood: 50, stone: 10 } },
                    { id: 11, server_id: 27, name: "Faster Windmill", limitGroup: 3, limit: 7, sandboxLimit: 299, cost: { wood: 60, stone: 20 } },
                    { id: 12, server_id: 28, name: "Power Mill", limitGroup: 3, limit: 7, sandboxLimit: 299, cost: { wood: 100, stone: 50 } },
                ],
                FARMS: [
                    { id: 13, server_id: 29, name: "Mine", limitGroup: 4, limit: 1, cost: { wood: 20, stone: 100 } },
                    { id: 14, server_id: 30, name: "Sapling", limitGroup: 5, limit: 2, cost: { wood: 150 } },
                ],
                TRAPS: [
                    { id: 15, server_id: 31, name: "Pit Trap", limitGroup: 6, limit: 6, cost: { wood: 30, stone: 30 } },
                    { id: 16, server_id: 32, name: "Boost Pad", limitGroup: 7, limit: 12, sandboxLimit: 299, cost: { wood: 5, stone: 20 } },
                ],
                EXTRAS: [
                    { id: 17, server_id: 33, name: "Turret", limitGroup: 8, limit: 2, cost: { wood: 200, stone: 150 } },
                    { id: 18, server_id: 34, name: "Platform", limitGroup: 9, limit: 12, cost: { wood: 20 } },
                    { id: 19, server_id: 35, name: "Healing Pad", limitGroup: 10, limit: 4, cost: { food: 10, wood: 30 } },
                    { id: 21, server_id: 37, name: "Blocker", limitGroup: 11, limit: 3, cost: { wood: 30, stone: 25 } },
                    { id: 22, server_id: 38, name: "Teleporter", limitGroup: 12, limit: 2, sandboxLimit: 299, cost: { wood: 60, stone: 60 } },
                ],
                SPAWN_PADS: [
                    { id: 20, server_id: 36, name: "Spawn Pad", limitGroup: 13, limit: 1, cost: { wood: 100, stone: 100 } },
                ],
            },

            /** @property {object} _issueTemplates - Holds raw markdown for pre-filling GitHub issue bodies. */
            _issueTemplates: {}, // MODIFIED: Will be populated by a fetch call.

            // NEW: Added URLs for fetching templates
            /** @property {object} _issueTemplateURLs - URLs to the raw issue templates on GitHub. */
            _issueTemplateURLs: {
                featureRequest: 'https://raw.githubusercontent.com/TimChinye/UserScripts/main/.github/RAW_ISSUE_TEMPLATE/feature_request.md',
                bugReport: 'https://raw.githubusercontent.com/TimChinye/UserScripts/main/.github/RAW_ISSUE_TEMPLATE/bug_report.md'
            },

            /** @property {object} _packetNames - Maps packet ID codes to human-readable names for logging. */
            _packetNames: {
                'io-init': 'Initial Connection',
                'A': 'All Clans List',
                'B': 'Disconnect',
                'C': 'Setup Game',
                'D': 'Add Player',
                'E': 'Remove Player',
                'G': 'Leaderboard Update',
                'H': 'Load Game Objects',
                'I': 'Update AI',
                'J': 'Animate AI',
                'K': 'Gather Animation',
                'L': 'Wiggle Game Object',
                'M': 'Shoot Turret',
                'N': 'Update Player Value',
                'O': 'Update Health',
                'P': 'Client Player Death',
                'Q': 'Kill Object',
                'R': 'Kill Objects',
                'S': 'Update Item Counts',
                'T': 'Update Age',
                'U': 'Update Upgrades',
                'V': 'Update Items',
                'X': 'Add Projectile',
                'Y': 'Remove Projectile',
                'Z': 'Server Shutdown Notice',
                'a': 'Update Players',
                'g': 'Add Alliance',
                '0': 'Ping Response',
                '1': 'Delete Alliance',
                '2': 'Alliance Notification',
                '3': 'Set Player Team',
                '4': 'Set Alliance Players',
                '5': 'Update Store Items',
                '6': 'Receive Chat',
                '7': 'Update Minimap',
                '8': 'Show Text',
                '9': 'Ping Map',
            },

            /** @property {object} _packetFormatters - Maps packet IDs to functions that format raw packet data into structured objects for easier use and logging. */
            _packetFormatters: {
                'io-init': ([socketID]) => ({ socketID }),
                'A': ([data]) => data,
                'B': ([reason]) => ({ reason }),
                'C': ([yourSID]) => ({ yourSID }),
                'D': ([playerData, isYou]) => ({
                    id: playerData[0], sid: playerData[1], name: playerData[2], x: playerData[3], y: playerData[4], dir: playerData[5], health: playerData[6], maxHealth: playerData[7], scale: playerData[8], skinColor: playerData[9], isYou
                }),
                'E': ([id]) => ({ id }),
                'G': (data) => {
                    const leaderboard = [];
                    for (let i = 0; i < data.length; i += 3) leaderboard.push({ sid: data[i], name: data[i + 1], score: data[i + 2] });
                    return { leaderboard };
                },
                'H': (data) => {
                    const objects = [];
                    for (let i = 0; i < data.length; i += 8) objects.push({ sid: data[i], x: data[i+1], y: data[i+2], dir: data[i+3], scale: data[i+4], type: data[i+5], itemID: data[i+6], ownerSID: data[i+7] });
                    return { objects };
                },
                'I': (data) => {
                    const ais = [];
                    for (let i = 0; i < data.length; i += 7) ais.push({ sid: data[i], index: data[i+1], x: data[i+2], y: data[i+3], dir: data[i+4], health: data[i+5], nameIndex: data[i+6] });
                    return { ais };
                },
                'J': ([sid]) => ({ sid }),
                'K': ([sid, didHit, index]) => ({ sid, didHit, weaponIndex: index }),
                'L': ([dir, sid]) => ({ dir, sid }),
                'M': ([sid, dir]) => ({ sid, dir }),
                'N': ([propertyName, value, updateView]) => ({ propertyName, value, updateView }),
                'O': ([sid, newHealth]) => ({ sid, newHealth }),
                'P': () => ({}),
                'Q': ([sid]) => ({ sid }),
                'R': ([sid]) => ({ sid }),
                'S': ([groupID, count]) => ({ groupID, count }),
                'T': ([xp, maxXP, age]) => ({ xp, maxXP, age }),
                'U': ([points, age]) => ({ points, age }),
                'V': ([items, isWeaponList]) => ({ items, isWeaponList }),
                'X': ([x, y, dir, range, speed, index, layer, sid]) => ({ x, y, dir, range, speed, index, layer, sid }),
                'Y': ([sid, newRange]) => ({ sid, newRange }),
                'Z': ([countdown]) => ({ countdown }),
                'a': (data) => {
                    const players = [];
                    for (let i = 0; i < data.length; i += 13) players.push({ sid: data[i], x: data[i+1], y: data[i+2], dir: data[i+3], buildIndex: data[i+4], weaponIndex: data[i+5], weaponVariant: data[i+6], team: data[i+7], isLeader: data[i+8], skinIndex: data[i+9], tailIndex: data[i+10], iconIndex: data[i+11], zIndex: data[i+12] });
                    return { players };
                },
                'g': ([clanData]) => ({ newClan: clanData }),
                '0': () => ({}),
                '1': ([sid]) => ({ sid }),
                '2': ([sid, name]) => ({ sid, name }),
                '3': ([team, isOwner]) => ({ team, isOwner }),
                '4': (data) => {
                    const members = [];
                    for (let i = 0; i < data.length; i += 2) members.push({ sid: data[i], name: data[i+1] });
                    return { members };
                },
                '5': ([action, itemID, itemType]) => {
                    const CoreC = window.MooMooUtilityMod.data.constants;
                    return ({
                        itemType: itemType === 0 ? CoreC.PACKET_DATA.WEARABLE_TYPES.HAT : CoreC.PACKET_DATA.WEARABLE_TYPES.ACCESSORY,
                        itemID,
                        action: action === 0 ? CoreC.PACKET_DATA.STORE_ACTIONS.ADD_ITEM : CoreC.PACKET_DATA.STORE_ACTIONS.UPDATE_EQUIPPED
                    });
                },
                '6': ([sid, message]) => ({ sid, message }),
                '7': (data) => ({ minimapData: data }),
                '8': ([x, y, value, type]) => ({ x, y, value, type }),
                '9': ([x, y]) => ({ x, y })
            },

            /**
             * Processes the raw item data from `_rawItems` into the lookup maps for efficient access.
             * This function is called once during the script's initialization.
             * @function
             * @returns {void}
             */
            initialize() {
                const CoreC = this.constants;
                const itemTypes = {
                    FOOD:              { slot: 0, itemType: CoreC.ITEM_TYPES.FOOD },
                    WALLS:             { slot: 1, itemType: CoreC.ITEM_TYPES.WALL },
                    SPIKES:            { slot: 2, itemType: CoreC.ITEM_TYPES.SPIKE },
                    WINDMILLS:         { slot: 3, itemType: CoreC.ITEM_TYPES.WINDMILL },
                    FARMS:             { slot: 6, itemType: CoreC.ITEM_TYPES.FARM },
                    TRAPS:             { slot: 4, itemType: CoreC.ITEM_TYPES.TRAP },
                    EXTRAS:            { slot: 5, itemType: CoreC.ITEM_TYPES.EXTRA },
                    SPAWN_PADS:        { slot: 7, itemType: CoreC.ITEM_TYPES.SPAWN_PAD },
                    PRIMARY_WEAPONS:   { slot: 8, itemType: CoreC.ITEM_TYPES.PRIMARY_WEAPON },
                    SECONDARY_WEAPONS: { slot: 9, itemType: CoreC.ITEM_TYPES.SECONDARY_WEAPON },
                };

                for (const category in this._rawItems) {
                    const { itemType, slot } = itemTypes[category];
                    this._rawItems[category].forEach(item => {
                        const fullItemData = {
                            ...item,

                            itemType,
                            slot,
                            cost: {
                                food: 0,
                                wood: 0,
                                stone: 0,
                                gold: 0,

                                ...item.cost
                            }
                        };

                        this._itemDataByServerId.set(fullItemData.server_id, fullItemData);

                        if (!this._itemDataBySlot.has(fullItemData.slot)) {
                            this._itemDataBySlot.set(fullItemData.slot, []);
                        }

                        this._itemDataBySlot.get(fullItemData.slot).push(fullItemData);
                    });
                }
            },
        },

        // --- PUBLIC UTILITY FUNCTIONS ---

        /**
         * Disables the entire utility mod, cleaning up all UI, styles, and event listeners.
         * @returns {void}
         */
        disableMod() {
            if (!this.state.enabled) return; // Already disabled
            Logger.warn("Disabling MooMoo Utility Mod...");
            this.state.enabled = false;

            // 1. Cleanup minimods first
            this.miniMods.forEach(mod => {
                if (typeof mod.cleanup === 'function') {
                    Logger.log(`Cleaning up minimod: ${mod.name}`);
                    try {
                        mod.cleanup();
                    } catch (e) {
                        Logger.error(`Error during cleanup of ${mod.name}:`, e);
                    }
                }
            });

            // 2. Cleanup core UI, styles, and observers
            const CoreC = this.data.constants;
            const style = document.getElementById(CoreC.DOM.UTILITY_MOD_STYLES);
            if (style) style.remove();

            const titleElem = document.getElementById(CoreC.DOM.GAME_TITLE);
            if (titleElem) titleElem.innerHTML = 'MOOMOO.io';

            const loadingInfo = document.getElementById(CoreC.DOM.LOADING_INFO);
            if (loadingInfo) loadingInfo.remove();

            this.state.observers.forEach(obs => obs.disconnect());
            this.state.observers.length = 0; // Clear the array

            // 3. Ensure all core UI element styles are unlocked
            this.waitForElementsToLoad({
                mainMenu: CoreC.DOM.MAIN_MENU,
                menuCardHolder: CoreC.DOM.MENU_CARD_HOLDER,
                loadingText: CoreC.DOM.LOADING_TEXT,
                gameUI: CoreC.DOM.GAME_UI,
                diedText: CoreC.DOM.DIED_TEXT,
            }).then(elements => {
                this.unlockStyleUpdates("display", Object.values(elements));
            });

            Logger.warn("Mod disabled. Game reverted to vanilla state.");
        },

        /**
         * Switches the UI to show the main menu.
         * @returns {void}
         */
        goToMainMenu() {
            this.setUIState('showMenu');
        },



        /**
         * Switches the UI to show the in-game interface.
         * @returns {void}
         */
        goToGamePlay() {
            this.setUIState('showGameplay');
        },

        /**
         * Extracts the server-side item ID from a DOM element's ID attribute.
         * @param {HTMLElement} itemElem - The action bar item element.
         * @returns {RegExpMatchArray|null} A match array or null.
         */
        getItemIdFromElem(itemElem) {
            return itemElem.id.match(this.data.constants.DOM.ACTION_BAR_ITEM_REGEX);
        },

        /**
         * Retrieves the full data object for an item from its corresponding DOM element.
         * @param {HTMLElement} itemElem - The action bar item element.
         * @returns {object|undefined} The item's data object.
         */
        getItemFromElem(itemElem) {
            const match = this.getItemIdFromElem(itemElem);
            if (!match) return undefined;

            const serverItemId = parseInt(match[1]);
            return this.data._itemDataByServerId.get(serverItemId);
        },

        /**
         * Checks if the player has sufficient resources to afford an item.
         * @param {object} itemData - The item's data object.
         * @returns {boolean} True if the player can afford the item.
         */
        isAffordableItem(itemData) {
            if (!itemData || !itemData.cost) return true; // Free items are always affordable

            return this.state.playerResources.food >= itemData.cost.food &&
                   this.state.playerResources.wood >= itemData.cost.wood &&
                   this.state.playerResources.stone >= itemData.cost.stone;
        },

        /**
         * Checks if an item element in the action bar is currently visible and represents a valid item.
         * @param {HTMLElement} itemElem - The action bar item element to check.
         * @returns {boolean} True if the item is available.
         */
        isAvailableItem(itemElem) {
            const isVisible = itemElem.style.display !== this.data.constants.CSS.DISPLAY_NONE;
            if (!isVisible) return false;

            return !!this.getItemIdFromElem(itemElem);
        },

        /**
         * Determines if an item can be equipped by checking its availability, affordability, and placement limits.
         * @param {HTMLElement} itemElem - The action bar item element to check.
         * @returns {boolean} True if all conditions are met.
         */
        isEquippableItem(itemElem) {
            if (!this.isAvailableItem(itemElem)) return false;

            const itemData = this.getItemFromElem(itemElem);
            if (!itemData) return false;

            // Check 1: Resource affordability
            if (!this.isAffordableItem(itemData)) return false;

            // Check 2: Placement limit
            if (itemData.limitGroup) {
                const limit = this.state.isSandbox && itemData.sandboxLimit ? itemData.sandboxLimit : itemData.limit;
                const currentCount = this.state.playerPlacedItemCounts.get(itemData.limitGroup) || 0;

                if (currentCount >= limit) return false;
            }

            return true; // If both checks pass
        },

        /**
         * Checks if a user input element is currently focused and visible.
         * @private
         * @returns {boolean} True if an input is focused.
         */
        isInputFocused() {
            const CoreC = this.data.constants;

            const isVisible = (id) => {
                const elem = document.getElementById(id);
                return elem && window.getComputedStyle(elem).display !== CoreC.CSS.DISPLAY_NONE && window.getComputedStyle(elem).opacity == CoreC.CSS.OPAQUE;
            };

            return this.state.focusableElementIds.some(isVisible);
        },

        /**
         * Registers a DOM element ID as a "focusable" element. When this element is visible,
         * most hotkeys will be disabled to prevent conflicts with typing or UI interaction.
         * @param {string} elementId - The ID of the DOM element to register.
         * @returns {void}
         */
        registerFocusableElement(elementId) {
            if (typeof elementId !== 'string' || !elementId) {
                Logger.error("registerFocusableElement: elementId must be a non-empty string.");
                return;
            }
            if (!this.state.focusableElementIds.includes(elementId)) {
                this.state.focusableElementIds.push(elementId);
                Logger.log(`Registered new focusable element: #${elementId}`);
            }
        },

        /**
         * Observes an element until its computed style 'display' is not 'none'.
         * @param {HTMLElement} element - The HTML element to observe.
         * @returns {Promise<HTMLElement>} A promise that resolves with the element.
         */
        waitForVisible(element) {
            if (!element) return Promise.reject();

            // Define the condition check in one place to avoid repetition.
            const isDisplayBlock = () => window.getComputedStyle(element).display !== 'none';

            // Handle the common case: If the element is already visible, resolve immediately.
            if (isDisplayBlock()) return Promise.resolve(element);

            // If not visible, return a promise that sets up the observer.
            return new Promise(resolve => {
                const observer = new MutationObserver(() => {
                    // When any mutation occurs, re-run the check.
                    if (isDisplayBlock()) {
                        // Once the condition is met, clean up and resolve the promise.
                        observer.disconnect();
                        resolve(element);
                    }
                });

                // Start observing the specific element for attribute changes
                observer.observe(element, { attributes: true });
                this.state.observers.push(observer);
            });
        },

        /**
         * Waits for one or more elements to be present in the DOM.
         *
         * @overload
         * @param {string} elementId - The ID of the single element to wait for.
         * @returns {Promise<HTMLElement>} A promise that resolves with the found HTML element.
         *
         * @overload
         * @param {string[]} elementIds - An array of element IDs to wait for.
         * @returns {Promise<HTMLElement[]>} A promise that resolves with an array of the found HTML elements, in the same order as the input array.
         *
         * @overload
         * @param {Object<string, string>} elementMap - An object mapping variable names to element IDs.
         * @returns {Promise<Object<string, HTMLElement>>} A promise that resolves with an object of the found HTML elements, keyed by the provided variable names.
         */
        waitForElementsToLoad(parameter, options = {}) {
            const { timeout = 5000 } = options; // Default timeout of 5 seconds.
            let inputType;
            let elementMap;

            // 1. Normalize Input (this part is largely the same, but slightly refined)
            if (typeof parameter === 'string') {
                inputType = 'string';
                elementMap = { [parameter]: parameter };
            } else if (Array.isArray(parameter)) {
                inputType = 'array';
                // A more modern/declarative way to convert an array to a map
                elementMap = Object.fromEntries(parameter.map(id => [id, id]));
            } else if (typeof parameter === 'object' && parameter !== null && !Array.isArray(parameter)) {
                inputType = 'object';
                elementMap = parameter;
            } else {
                return Promise.reject(new TypeError('Invalid argument. Must be a string, array of strings, or an object.'));
            }

            // 2. Core Waiting Logic (significantly improved)
            const corePromise = new Promise((resolve, reject) => {
                const foundElements = {};
                // Use a Set for efficient lookup and deletion of keys we still need to find.
                const remainingKeys = new Set(Object.keys(elementMap));
                let observer; // Declare here to be accessible in timeout

                const timeoutId = setTimeout(() => {
                    observer?.disconnect(); // Stop observing on timeout
                    const missingIds = Array.from(remainingKeys).map(key => elementMap[key]);
                    reject(new Error(`Timed out after ${timeout}ms. Could not find elements with IDs: ${missingIds.join(', ')}`));
                }, timeout);

                const checkElements = () => {
                    // Only iterate over the keys of elements we haven't found yet.
                    for (const key of remainingKeys) {
                        const id = elementMap[key];
                        const element = document.getElementById(id);
                        if (element) {
                            foundElements[key] = element;
                            remainingKeys.delete(key); // KEY IMPROVEMENT: Stop looking for this element.
                        }
                    }

                    // If the set is empty, we've found everything.
                    if (remainingKeys.size === 0) {
                        clearTimeout(timeoutId); // Success, so clear the timeout.
                        observer?.disconnect();
                        resolve(foundElements);
                    }
                };

                // Set up the observer to only call our efficient checker.
                observer = new MutationObserver(checkElements);

                // Perform an initial check in case elements are already on the page.
                checkElements();

                // If the initial check didn't find everything, start observing.
                if (remainingKeys.size > 0) {
                    observer.observe(document.body, { childList: true, subtree: true });
                    // Assuming 'this.state.observers' exists from your original context
                    if (this.state && this.state.observers) {
                        this.state.observers.push(observer);
                    }
                }
            });

            // 3. Format Output (unchanged, but now attached to the more robust promise)
            return corePromise.then(foundElements => {
                switch (inputType) {
                    case 'string':
                        return Object.values(foundElements)[0];
                    case 'array':
                        return parameter.map(id => foundElements[id]);
                    case 'object':
                        return foundElements;
                    default:
                        throw new Error(`Internal Error: Unhandled inputType "${inputType}" in waitForElementsToLoad.`);
                }
            });
        },

        /**
         * Locks a CSS style property on an array of elements, preventing it from being
         * changed by JavaScript. A 'data-locked-styles' attribute is added to the element
         * for easy inspection, acting as the single source of truth for the lock state.
         *
         * @param {string} propertyName - The name of the style property to lock.
         * @param {HTMLElement[]} elements - An array of HTMLElements to affect.
         * @returns {void}
         */
        lockStyleUpdates(propertyName, elements) {
            if (!Array.isArray(elements)) {
                console.error("Failed to lock style: `elements` must be an array.");
                return;
            }

            elements.forEach(element => {
                if (!(element instanceof HTMLElement)) {
                    console.warn("Skipping item because it is not a valid HTMLElement:", element);
                    return;
                }

                const lockedStyles = (element.getAttribute('data-locked-styles') || '').split(',').filter(Boolean);
                if (lockedStyles.includes(propertyName)) {
                    return; // This property is already locked.
                }

                const styleObj = element.style;
                // Capture the value at the moment of locking.
                let currentValue = styleObj[propertyName];

                Object.defineProperty(styleObj, propertyName, {
                    // This MUST be true so we can 'delete' it later to unlock.
                    configurable: true,
                    enumerable: true,
                    get() {
                        return currentValue;
                    },
                    set(newValue) {
                        console.warn(`Blocked attempt to set locked property "${propertyName}" to "${newValue}" on`, element);
                        // The set operation is completely ignored.
                    }
                });

                // Update the visible HTML attribute.
                lockedStyles.push(propertyName);
                element.setAttribute('data-locked-styles', lockedStyles.join(','));
            });
        },

        /**
        * Unlocks a CSS style property on an array of elements, allowing to be changed by JavaScript. The 'data-locked-styles'
        * attribute is updated or removed.
        *
        * @param {string} propertyName - The name of the style property to unlock.
        * @param {HTMLElement[]} elements - An array of HTMLElements to affect.
        * @returns {void}
        */
        unlockStyleUpdates(propertyName, elements) {
            if (!Array.isArray(elements)) {
                console.error("Failed to unlock style: `elements` must be an array.");
                return;
            }

            elements.forEach(element => {
                if (!(element instanceof HTMLElement)) {
                    return;
                }

                const lockedStylesAttr = element.getAttribute('data-locked-styles');
                if (!lockedStylesAttr || !lockedStylesAttr.includes(propertyName)) {
                    return; // This property isn't locked on this element.
                }

                // --- The Key Step: Delete the override ---
                // This removes our custom get/set and reverts to the default prototype behavior.
                delete element.style[propertyName];

                // Update the visible HTML attribute.
                const updatedLockedStyles = lockedStylesAttr.split(',').filter(p => p !== propertyName);

                if (updatedLockedStyles.length > 0) {
                    element.setAttribute('data-locked-styles', updatedLockedStyles.join(','));
                } else {
                    element.removeAttribute('data-locked-styles');
                }
            });
        },

        /**
         * Returns a Promise that resolves on the next animation frame.
         * @returns {Promise<void>}
         */
        waitTillNextFrame() {
            return new Promise(resolve => requestAnimationFrame(resolve));
        },

        /**
         * Returns a Promise that resolves after a specified delay.
         * @param {number} ms - The delay in milliseconds.
         * @returns {Promise<void>}
         */
        wait(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
        },

        // --- CORE INTERNAL FUNCTIONS ---

        /**
         * A simple parser for the navigator.userAgent string.
         * @private
         * @returns {{name: string, version: string}}
         */
        _getBrowserInfo() {
            const ua = navigator.userAgent;
            let match = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
            let temp;

            if (/trident/i.test(match[1])) {
                temp = /\brv[ :]+(\d+)/g.exec(ua) || [];
                return { name: 'IE', version: temp[1] || '' };
            }
            if (match[1] === 'Chrome') {
                temp = ua.match(/\b(OPR|Edge)\/(\d+)/);
                if (temp != null) return { name: temp[1].replace('OPR', 'Opera'), version: temp[2] };
            }
            match = match[2] ? [match[1], match[2]] : [navigator.appName, navigator.appVersion, '-?'];
            if ((temp = ua.match(/version\/(\d+)/i)) != null) match.splice(1, 1, temp[1]);

            return { name: match[0], version: match[1] };
        },

        /**
         * Encodes and sends a packet to the game server.
         * @param {string} type - The one-character packet identifier.
         * @param {any[]} data - The payload data for the packet.
         * @returns {void}
         */
        sendGamePacket(type, data) {
            if (!this.state.enabled) return; // Already disabled, no need to proceed.

            const CoreC = this.data.constants;
            try {
                if (this.state.gameSocket && this.state.gameSocket.readyState === CoreC.GAME_STATE.WEBSOCKET_STATE_OPEN) {
                    this.state.gameSocket.send(this.state.gameEncoder.encode([type, data]));
                }
            } catch (err) {
                Logger.error(`Failed to send packet [${type}]`, err);
            }
        },

        /**
         * Intercepts and processes incoming WebSocket messages to track game state changes.
         * @param {MessageEvent} event - The WebSocket message event containing the raw game data.
         * @returns {void}
         */
        handleSocketMessage(event) {
            if (!this.state.enabled || !this.state.gameDecoder) return; // Already disabled or already set up, no need to proceed.

            try {
                const [packetID, ...argsArr] = this.state.gameDecoder.decode(new Uint8Array(event.data));
                const args = argsArr[0]; // The game nests args in another array for some reason
                const packetName = this.data._packetNames[packetID] || 'Unknown Packet';
                const packetData = this.data._packetFormatters[packetID] ? this.data._packetFormatters[packetID](args) : { rawData: args };

                // Dispatch the packet to all minimods
                this.miniMods.forEach(mod => {
                    if (typeof mod.onPacket === 'function') {
                        mod.onPacket(packetName, packetData, args);
                    }
                });

                switch (packetName) {
                    case 'Client Player Death': {
                        if (this.state.playerHasRespawned); // Do nothing
                        else this.state.playerHasRespawned = true

                        break;
                    }

                    case 'Server Shutdown Notice': {
                        const { countdown } = packetData;
                        const CoreC = this.data.constants;
                        const shutdownDisplay = document.getElementById(CoreC.DOM.SHUTDOWN_DISPLAY);

                        if (countdown < 0 || !shutdownDisplay) return;

                        var minutes = Math.floor(countdown / 60);
                        var seconds = countdown % 60;
                        seconds = ("0" + seconds).slice(-2);

                        shutdownDisplay.innerText = "Server restarting in " + minutes + ":" + seconds;
                        shutdownDisplay.hidden = false;

                        break;
                    }
                }

                if (this.config.DEBUG_MODE) {
                    // --- Log Every Packet ---

                    /* Ignore List (mostly due to spam):
                    {
                        'I': 'All Animals / NPCs State Update',
                        'a': 'All Players State Update',
                        '0': 'Ping',
                        '7': 'Unknown Periodic Event'
                        'H': 'Create Map Objects',
                        'G': 'Leaderboard Update',
                        'K': 'Player Attack Animation',
                        'L': 'Object Damaged',
                        'T': 'Player XP Update / Age Up',
                    }
                    */

                    // These four periodically spam, very quickly too.
                    // const ignoredPackets = ['I', 'a', '0', '7', 'Z'];
                    // Some of these are period, some aren't, all are very frequent.
                    const ignoredPackets = ['I', 'a', '0', '7', 'Z', 'H', 'G', 'K', 'L', 'T'];
                    if (ignoredPackets.includes(packetID.toString())) return;
                    // Other people get hurt / heal around you quite often, it's a little annoying:
                    // if (packetID.toString() === 'O' && packetData.playerID !== this.state.playerId) return;

                    const dataString = Object.keys(packetData).length > 0 ? JSON.stringify(packetData) : '{}';
                    Logger.log(`Packet: ${packetName} (${packetID}) -> ${dataString}`, args);
                }
            } catch (e) { /* Ignore decoding errors for packets we don't care about */
                if (this.config.DEBUG_MODE) Logger.error("Failed to decode packet:", event, e);
            }
        },

        // --- INITIALIZATION & HOOKING ---

        // NEW: Fetches issue templates from GitHub to ensure they are always up-to-date.
        async getIssueTemplates() {
            Logger.log("Fetching issue templates from GitHub...");
            const urls = this.data._issueTemplateURLs;
            try {
                // Fetch both templates concurrently for speed
                const [featureText, bugText] = await Promise.all([
                    fetch(urls.featureRequest).then(res => res.ok ? res.text() : ''),
                    fetch(urls.bugReport).then(res => res.ok ? res.text() : '')
                ]);
                this.data._issueTemplates.featureRequest = featureText;
                this.data._issueTemplates.bugReport = bugText;

                if (featureText && bugText) {
                    Logger.log("Successfully fetched issue templates.", "color: #4CAF50;");
                } else {
                    Logger.warn("One or more issue templates failed to load. Links will fall back to default.");
                }
            } catch (error) {
                Logger.error("Failed to fetch issue templates:", error);
                // Ensure the templates object is clean on error
                this.data._issueTemplates = { featureRequest: '', bugReport: '' };
            }
        },

        /**
         * Collects and injects CSS from the core mod and all registered mini-mods.
         * @returns {void}
         */
        injectCSS() {
            const CoreC = this.data.constants;
            const allCSS = [];

            // Add core CSS
            const coreCSS = this.applyCoreCSS().trim();
            if (coreCSS) {
                allCSS.push('/* --- Injecting Core Mod CSS --- */\n' + coreCSS);
            }

            // Add minimod CSS
            this.miniMods.forEach(mod => {
                if (mod && typeof mod.applyCSS === 'function') {
                    const modCSS = mod.applyCSS().trim();
                    if (modCSS) {
                        allCSS.push('/* --- Injecting "' + (mod.name || 'Unnamed Mod') + '" MiniMod CSS --- */\n' + modCSS);
                }
                }
            });

            if (allCSS.length > 0) {
                const style = document.createElement('style');
                style.id = CoreC.DOM.UTILITY_MOD_STYLES;
                style.textContent = allCSS.join('\n\n/* --- CSS Separator --- */\n\n');
                document.head.append(style);
                Logger.log(`Injected CSS from core and ${this.miniMods.filter(m => typeof m.applyCSS === 'function' && m.applyCSS().trim()).length} mini-mod(s).`, "color: #4CAF50;");
            } else {
                Logger.log("No CSS to inject.");
            }
        },

        /**
         * Provides the CSS styles to be applied for the core mod.
         * @returns {string} The CSS string.
         */
        applyCoreCSS() {
            const CoreC = this.data.constants;
            return `
                #${CoreC.DOM.GAME_TITLE} {
                    --text-shadow-colour: oklch(from currentColor calc(l * 0.82) c h);
                    text-shadow: 0 1px 0 var(--text-shadow-colour),   0 2px 0 var(--text-shadow-colour),   0 3px 0 var(--text-shadow-colour),   0 4px 0 var(--text-shadow-colour),   0 5px 0 var(--text-shadow-colour),   0 6px 0 var(--text-shadow-colour),   0 7px 0 var(--text-shadow-colour),   0 8px 0 var(--text-shadow-colour),   0 9px 0 var(--text-shadow-colour);
                
                    & > span {
                        color: oklch(0.95 0.05 92.5);
                    }
                }

                #${CoreC.DOM.LOADING_INFO} {
                    color: #fff;
                    text-align: center;
                    font-size: 22.5px;
                }

                #${CoreC.DOM.AD_HOLDER} {
                    display: block;

                    & > .${CoreC.DOM.MENU_CARD_CLASS} {
                        margin: 0;
                    }
                }

                button.${CoreC.DOM.MENU_LINK_CLASS} {
                    color: #a56dc8;
                    text-decoration: none;
                    background: none;
                    border: none;
                    padding: 0;
                    cursor: pointer;
                    
                    &:hover {
                        color: #795094;
                    }
                }

                #${CoreC.DOM.MAIN_MENU}.${CoreC.DOM.PASSTHROUGH_CLASS} {
                    pointer-events: none;

                    #${CoreC.DOM.MENU_CONTAINER} ~ div {
                        pointer-events: auto;
                    }
                }
            `;
        },

        /**
         * Updates the main menu screen.
         * @returns {void}
         */
        updateMainMenu() {
            const CoreC = this.data.constants;

            this.waitForElementsToLoad(CoreC.DOM.GAME_TITLE).then((titleElem) => {
                if (!this.state.enabled || !window.gmInfo) return;
                titleElem.innerHTML = `MOOMOO<span>.</span>io`;

                const linksContainer = document.getElementById(CoreC.DOM.LINKS_CONTAINER);

                // --- MODIFIED: Dynamic Link Generation ---
                const gmInfo = window.gmInfo;
                const featureTemplate = this.data._issueTemplates.featureRequest;
                const bugTemplate = this.data._issueTemplates.bugReport;

                // 1. Define fallback URLs
                let featureRequestURL = 'https://github.com/TimChinye/UserScripts/issues/new?template=feature_request.md';
                let bugReportURL = 'https://github.com/TimChinye/UserScripts/issues/new?template=bug_report.md';

                // 2. If templates were fetched successfully, create pre-filled URLs
                if (featureTemplate && bugTemplate) {
                    const scriptNameVersion = `${gmInfo.script.name} (v${gmInfo.script.version})`;
                    const browserInfo = this._getBrowserInfo();
                    const environmentDetails = `
- **Browser Name:** <!-- (Required) e.g; Chrome, Firefox, Edge, Safari -->
  > ${browserInfo.name}
- **Browser Version:** <!-- (Optional) e.g; 125.0 -->
  > ${browserInfo.version}
- **Userscript Manager Name:** <!-- (Optional) e.g; Tampermonkey, Violentmonkey -->
  > ${gmInfo.scriptHandler}
- **Userscript Manager Version:** <!-- (Optional) e.g; 5.1.1 -->
  > ${gmInfo.version}
                    `.trim();

                    const featureBody = featureTemplate.replace('{{ SCRIPT_NAME_VERSION }}', scriptNameVersion);
                    const bugBody = bugTemplate.replace('{{ SCRIPT_NAME_VERSION }}', scriptNameVersion).replace('{{ ENVIRONMENT_DETAILS }}', environmentDetails);

                    featureRequestURL += `&body=${encodeURIComponent(featureBody)}`;
                    bugReportURL += `&body=${encodeURIComponent(bugBody)}`;
                }

                // 3. Inject the final HTML with the correct URLs
                linksContainer.insertAdjacentHTML('beforebegin', `
                    <div id="linksContainer1">
                        <a href="https://greatest.deepsurf.us/en/scripts/463689/feedback" target="_blank" class="menuLink">Share Thoughts</a>
                         |
                        <a href="${featureRequestURL}" target="_blank" class="menuLink">Got an idea?</a>
                         |
                        <a href="${bugReportURL}" target="_blank" class="menuLink">Report a Bug</a>
                         |
                        <a href="https://github.com/TimChinye/UserScripts/commits/main/MooMoo.io%20Utility%20Mod/script.user.js" target="_blank" class="menuLink">v${gmInfo.script.version}</a>
                    </div>
                `);

                linksContainer.firstElementChild.insertAdjacentHTML('afterend', ' | <a href="https://frvr.com/browse" target="_blank" class="menuLink">Other Games</a>');
            });
        },

        /**
         * Manages the visibility of core game UI screens.
         * @param {'showError' | 'showGameplay' | 'showMenu'} state - The UI state to display.
         * @returns {void}
         */
        setUIState(state) {
            const CoreC = this.data.constants;
            const elementIds = {
                mainMenu: CoreC.DOM.MAIN_MENU,
                menuCardHolder: CoreC.DOM.MENU_CARD_HOLDER,
                loadingText: CoreC.DOM.LOADING_TEXT,
                gameUI: CoreC.DOM.GAME_UI,
                diedText: CoreC.DOM.DIED_TEXT,
            };

            this.waitForElementsToLoad(elementIds).then(elementsMap => {
                const domElements = Object.values(elementsMap);

                // Ensure styles are unlocked before changing them.
                this.unlockStyleUpdates("display", domElements);

                // Reset all to a blank slate.
                domElements.forEach(el => el.style.display = 'none');
                elementsMap.mainMenu.classList.remove(CoreC.DOM.PASSTHROUGH_CLASS);

                // Show only the elements necessary for each screen
                switch (state) {
                    case 'showMenu':
                        elementsMap.mainMenu.style.display = 'block';
                        elementsMap.menuCardHolder.style.display = 'block';
                        break;

                    case 'showGameplay':
                        elementsMap.gameUI.style.display = 'block';
                        elementsMap.menuCardHolder.style.display = 'block';
                        break;

                    case 'showError':
                        elementsMap.mainMenu.style.display = 'block';
                        elementsMap.loadingText.style.display = 'block';
                        elementsMap.mainMenu.classList.add(CoreC.DOM.PASSTHROUGH_CLASS);

                        if (this.state.enabled) {
                            // Disable updating the element display types
                            this.lockStyleUpdates("display", domElements);

                            // Provide useful info to the user.
                            const loadingInfo = document.getElementById(CoreC.DOM.LOADING_INFO);
                            if (loadingInfo) elementsMap.loadingText.childNodes[0].nodeValue = `Re-attempting Connection...`;
                        }
                        break;

                    default:
                        Logger.error(`Invalid UI state provided: ${state}`);
                        break;
                }
            });
        },

        /**
         * Updates the loading/error UI screen with a message to provide feedback during connection attempts.
         * @private
         * @param {string} message - The message to display.
         * @returns {void}
         */
        _updateLoadingUI(message) {
            const CoreC = this.data.constants;

            // Inject custom info element for the reload logic
            const getLoadingInfoElem = () => document.getElementById(CoreC.DOM.LOADING_INFO);
            const menuContainer = document.getElementById(CoreC.DOM.MENU_CONTAINER);
            if (menuContainer && !getLoadingInfoElem()) {
                menuContainer.insertAdjacentHTML('beforeend', `<div id="${CoreC.DOM.LOADING_INFO}" style="display: none;"><br>${message}<br></div>`);

                const loadingText = document.getElementById(CoreC.DOM.LOADING_TEXT);
                const syncDisplayCallback = () => {
                    const newDisplayStyle = window.getComputedStyle(loadingText).display;
                    const loadingInfo = getLoadingInfoElem();
                    if (loadingInfo && loadingInfo.style.display !== newDisplayStyle) {
                        loadingInfo.style.display = newDisplayStyle;
                    }
                };

                const observer = new MutationObserver(syncDisplayCallback);
                observer.observe(loadingText, { attributes: true, attributeFilter: ['style'] });
                this.state.observers.push(observer);
            }
        },

        /**
         * Handles the scenario where the script fails to hook codecs and prompts for a reload.
         * If reload prompt is cancelled, disables mod.
         * @private
         * @param {boolean} [afterGameEnter=false] - If true, indicates failure happened after entering the game.
         * @returns {Promise<void>}
         */
        async handleHookFailureAndReload(afterGameEnter = false) {
            if (!this.state.enabled) return; // Already disabled, no need to proceed.

            const CoreC = this.data.constants;

            const { gameUI, mainMenu } = await this.waitForElementsToLoad({
                gameUI: CoreC.DOM.GAME_UI,
                mainMenu: CoreC.DOM.MAIN_MENU
            });

            if (afterGameEnter) await this.waitForVisible(gameUI);
            else await this.waitForVisible(mainMenu);

            Logger.error("All hooking methods failed. The script cannot function. Reloading...");
            this._updateLoadingUI("Couldn't intercept in time. May be a network issue. Try not entering the game so fast.");
            this.setUIState('showError');

            await this.wait(5000);

            const loadingInfo = document.getElementById(CoreC.DOM.LOADING_INFO);
            if (loadingInfo) {
                loadingInfo.append("If you cancel, you can play the game as normal - without the mod enabled.");

                await this.waitTillNextFrame();
                await this.waitTillNextFrame();
            }

            if (afterGameEnter || window.confirm("Are you sure you want to reload?")) window.location.reload();

            // User cancelled the reload. Disable the mod and restore the UI - play like normal.
            Logger.warn("User cancelled reload. Disabling mod.");
            this.disableMod();

            if (afterGameEnter) {
                this.setUIState('showGameplay');
            } else {
                this.setUIState('showMenu');
            }
        },

        /**
         * Checks if codecs and WebSocket are ready and performs final setup.
         * @private
         * @returns {void}
         */
        attemptFinalSetup() {
            if (!this.state.enabled || !this.state.codecsReady || !this.state.socketReady) return; // Already disabled or already set up, no need to proceed.

            Logger.log("Codecs and WebSocket are ready. Attaching all listeners.", "color: #ffb700;");
            this.state.gameSocket.addEventListener('message', this.handleSocketMessage.bind(this));
            this.miniMods.forEach(mod => {
                if (typeof mod.addEventListeners === 'function') mod.addEventListeners();
            });
        },

        /**
         * Finds game's msgpack instances by hooking into Object.prototype.
         * @private
         * @param {string} propName - The unique property name to watch for.
         * @param {Function} onFound - The callback to execute when the object is found.
         * @returns {void}
         */
        hookIntoPrototype(propName, onFound) {
            if (!this.state.enabled) return; // Already disabled, no need to proceed.

            Logger.log(`Setting up prototype hook for: ${propName}`);
            if (this.state?.codecsReady) return Logger.log("Both codecs found already, cancelling prototype hooks method.", "color: #4CAF50;");

            const originalDesc = Object.getOwnPropertyDescriptor(Object.prototype, propName);
            Object.defineProperty(Object.prototype, propName, {
                set(value) {
                    if (!MooMooUtilityMod.state.enabled) return; // Already disabled, no need to proceed.
                    if (MooMooUtilityMod.state.codecsReady) return Logger.log("Both codecs found already, cancelling prototype hooks method.", "color: #4CAF50;");

                    // Restore the prototype to its original state *before* doing anything else.
                    // This prevents unexpected side effects and race conditions within the hook itself.
                    if (originalDesc) {
                        Object.defineProperty(Object.prototype, propName, originalDesc);
                    } else {
                        delete Object.prototype[propName];
                    }

                    // Now, apply the value to the current instance.
                    this[propName] = value;

                    // Check if this is the object we are looking for and trigger the callback.
                    // We check for the function's existence to be more certain.
                    const isFoundCodec = (targetPropName, codecOperation) => propName === targetPropName && typeof codecOperation === 'function';
                    if (isFoundCodec("initialBufferSize", this.encode) || isFoundCodec("maxStrLength", this.decode)) {
                        Logger.log(`Hook successful for "${propName}". Object found.`, "color: #4CAF50;");
                        onFound(this);
                    }
                },
                configurable: true,
            });
        },

        /**
         * Sets up hooks to capture the game's msgpack encoder and decoder instances.
         * @private
         * @returns {void}
         */
        initializeHooks() {
            // Set up prototype hooks for both encoder and decoder
            const onCodecFound = () => {
                if (this.state.gameEncoder && this.state.gameDecoder) {
                    Logger.log(`Both msgpack codecs found via prototype hooks. ${Date.now() - this.state.initTimestamp}ms`, "color: #4CAF50;");

                    this.state.codecsReady = true;

                    this.attemptFinalSetup();
                }
            };

            this.hookIntoPrototype("initialBufferSize", (obj) => { this.state.gameEncoder = obj; onCodecFound(); });
            this.hookIntoPrototype("maxStrLength", (obj) => { this.state.gameDecoder = obj; onCodecFound(); });
        },

        /**
         * Intercepts and modifies the game's main script to expose codecs.
         * @private
         * @returns {void}
         */
        interceptGameScript() {
            if (!this.state.enabled) return; // Already disabled, no need to proceed.
            Logger.log("Attempting to intercept and modify the game script...");

            const CoreC = this.data.constants;

            const SCRIPT_SELECTOR = "/assets/index-eb87bff7.js";
            const ENCODER_REGEX = /(this\.initialBufferSize=\w,)/;
            const ENCODER_EXPOSURE = `$1 (typeof Logger !== 'undefined' && Logger.log("✅ CAPTURED ENCODER!")), window.gameEncoder = this,`;
            const DECODER_REGEX = /(this\.maxStrLength=\w,)/;
            const DECODER_EXPOSURE = `$1 (typeof Logger !== 'undefined' && Logger.log("✅ CAPTURED DECODER!")), window.gameDecoder = this,`;

            /**
             * Attempts to find and modify the game script to expose the codecs.
             * If found, it disconnects the observer to prevent further attempts.
             * @param {MutationObserver} [observer] - The MutationObserver instance to disconnect if the script is found.
             */
            const leaveBackdoorOpen = (observer) => {
                if (!this.state.enabled) return; // Already disabled, no need to proceed.
                if (this.state?.codecsReady) return Logger.log("Both codecs found already, cancelling prototype hooks method.", "color: #4CAF50;");

                const targetScript = document.querySelector(`script[src*="${SCRIPT_SELECTOR}"]`);
                if (targetScript) {
                    if (observer) observer.disconnect();

                    Logger.log(`Found game script: ${targetScript.src}`);
                    targetScript.type = 'text/plain'; // Neutralize the original script

                    fetch(targetScript.src)
                    .then(res => res.text())
                    .then(scriptText => {
                        if (!this.state.enabled) return; // Already disabled, no need to proceed.
                        if (this.state?.codecsReady) return Logger.log("Both codecs found already, cancelling prototype hooks method.", "color: #4CAF50;");

                        let modifiedScript = scriptText
                            .replace(/(customElements\.define\("altcha-widget".*"verify"\],!1\)\);)/, '')
                            .replace(ENCODER_REGEX, ENCODER_EXPOSURE)
                            .replace(DECODER_REGEX, DECODER_EXPOSURE);

                        if (!modifiedScript.includes("window.gameEncoder") || !modifiedScript.includes("window.gameDecoder")) return Logger.error("Script injection failed! Regex patterns did not match.");

                        const newScript = document.createElement('script');
                        newScript.id = CoreC.DOM.UTILITY_MOD_SCRIPTS;
                        newScript.textContent = modifiedScript;

                        // This is the function we want to run once the DOM is ready.
                        const injectAndFinalize = () => {
                            if (!this.state.enabled) return; // Already disabled, no need to proceed.
                            if (this.state?.codecsReady) return Logger.log("Both codecs found already, cancelling prototype hooks method.", "color: #4CAF50;");

                            // Make sure this only runs once, in case of any edge cases.
                            if (document.body.contains(newScript)) return;

                            document.head.append(newScript);
                            targetScript.remove();

                            Logger.log("Modified game script injected.", "color: #4CAF50;");

                            // Verify capture and finalize setup
                            // Use setTimeout to allow the newly injected script to execute and populate the window object.
                            setTimeout(() => {
                                if (window.gameEncoder && window.gameDecoder) {
                                    Logger.log(`Codec interception successful! ${Date.now() - this.state.initTimestamp}ms`, "color: #4CAF50; font-weight: bold;");

                                    this.state.gameEncoder = window.gameEncoder;
                                    this.state.gameDecoder = window.gameDecoder;
                                    this.state.codecsReady = true;

                                    this.attemptFinalSetup();
                                } else {
                                    Logger.error("Codecs were not found on window after injection.");
                                }
                            }, 0);
                        };

                        // Check if the DOM is already loaded
                        if (document.readyState === 'loading') {
                            // DOM is not ready yet, so wait for the event
                            document.addEventListener('DOMContentLoaded', injectAndFinalize);
                        } else {
                            // DOM is already ready, so execute the function immediately
                            injectAndFinalize();
                        }
                    })
                    .catch(err => {
                        Logger.error("Failed to fetch or process game script:", err);
                    });
                } else { /* Fail silently */ };
            }

            const observer = new MutationObserver((mutations, obs) => leaveBackdoorOpen(obs));
            observer.observe(document.documentElement, { childList: true, subtree: true });
            this.state.observers.push(observer);
        },

        /**
         * Sets up a WebSocket proxy to capture the game's connection instance.
         * @private
         * @returns {void}
         */
        setupWebSocketProxy() {
            const originalWebSocket = window.WebSocket;
            window.WebSocket = new Proxy(originalWebSocket, {
                construct: (target, args) => {
                    const wsInstance = new target(...args);

                    if (this.state.enabled) {
                        if (this.state.gameEncoder && this.state.gameDecoder) {
                            this.state.gameSocket = wsInstance;
                            this.state.socketReady = true;

                            Logger.log("Game WebSocket instance captured.");
                            window.WebSocket = originalWebSocket; // Restore immediately
                            this.attemptFinalSetup();
                        }
                        else {
                            // A final check. If by the time the WS is created NO method has worked, fail.
                            console.error("WebSocket created but codecs were not found. All hooking methods have failed.");
                            this.handleHookFailureAndReload(true);
                        }
                    }

                    return wsInstance;
                }
            });
        },

        /**
         * Runs once the player has spawned, notifying minimods that the game is fully ready.
         * @private
         * @returns {void}
         */
        onGameReady() {
            if (!this.state.enabled) return; // Already disabled, no need to proceed.

            try {
                // Notify minimods that the game is ready
                this.miniMods.forEach(mod => {
                    if (typeof mod.onGameReady === 'function') mod.onGameReady();
                });

                const shutdownDisplay = document.getElementById(this.data.constants.DOM.SHUTDOWN_DISPLAY);
                if (shutdownDisplay) shutdownDisplay.hidden = false;
            } catch(e) {
                Logger.error("Failed during onGameReady setup.", e);
            }
        },

        /**
         * The main entry point for the script.
         * @returns {void}
         */
        init() {
            // Exposes the logger to the global window object for debugging purposes.
            window.Logger = Logger;

            getGMInfo().then((gmInfo) => {
                Logger.log(`--- MOOMOO.IO Utility Mod (v${gmInfo.script.version}) Initializing ---`, "color: #ffb700; font-weight: bold;");
                window.gmInfo = gmInfo;
            })

            // Attempts to find codecs by modifying the game script directly to open a backdoor.
            this.interceptGameScript(); // Typically succeeds 0.025x slower than mainMenu.
            
            // Set up hooks to intercept codecs as they enter the global scope.
            this.initializeHooks(); // Typically succeeds 0.5x slower than mainMenu.
            
            // Set up WebSocket proxy to capture the game's WebSocket instance.
            this.setupWebSocketProxy();
            
            const CoreC = this.data.constants;

            // If codecs aren't found within a reasonable amount of time, assume failure and prompt for reload.
            this.waitForElementsToLoad({ mainMenu: this.data.constants.DOM.MAIN_MENU }).then(({ mainMenu }) => {
                // We use time until main menu is loaded & visible, to get a good baseline for CPU/Network speeds.
                this.waitForVisible(mainMenu).then(() => {
                    setTimeout(() => {
                        if (!this.state.enabled || this.state.codecsReady) return; // Already disabled

                        Logger.error("Hooks failed to find codecs within the time limit.");
                        this.handleHookFailureAndReload();
                    }, ((Date.now() - this.state.initTimestamp) + 250) * 2.5); // If no success after 1.5x the mainMenu, assume failure.
                });
            });

            // Initialize item data and lookups
            this.data.initialize();

            // Inject styles immediately, as document.head is available early.
            this.injectCSS();

            // Wait for the body to load, and get issue templates before trying to update main menu.
            this.getIssueTemplates().then(() => {
                this.updateMainMenu();
            });
            
            this.state.focusableElementIds = [CoreC.DOM.CHAT_HOLDER, CoreC.DOM.STORE_MENU, CoreC.DOM.ALLIANCE_MENU];

            // Initialize all registered minimods
            this.miniMods.forEach(mod => {
                if (typeof mod.init === 'function') {
                    Logger.log(`Initializing minimod: ${mod.name || 'Unnamed Mod'}`);
                    try {
                        mod.init();
                    } catch (e) {
                        Logger.error(`Error during init of ${mod.name || 'Unnamed Mod'}:`, e);
                    }
                }
            });

            // Exposes the core to the global window object for debugging purposes.
            window.MooMooUtilityMod = this;
        },

        // --- MINI-MOD MANAGEMENT ---

        /** @property {Array<object>} miniMods - A list of all registered sub-modules (minimods). */
        miniMods: [],

        /**
         * Adds a mini-mod to the system.
         * @param {object} mod - The mini-mod object to register.
         * @returns {void}
         */
        registerMod(mod) {
            this.miniMods.push(mod);
            mod.core = this; // Give the minimod a reference to the core

            Logger.log(`Registered minimod: ${mod.name || 'Unnamed Mod'}`);
        }
    };

    /**
     * @module SettingsManagerMiniMod
     * @description Manages loading, saving, and displaying a UI for all mod settings.
     */
    const SettingsManagerMiniMod = {
        /** @property {object|null} core - A reference to the core module, set upon registration. */
        core: null,

        /** @property {string} name - The display name of the minimod. */
        name: "Settings Manager",

        /** @property {object} constants - Constants specific to this minimod. */
        constants: {
            LOCALSTORAGE_KEY: 'MooMooUtilMod_Settings',
            DOM: {
                MOD_CARD: 'modCard',
                LEFT_CARD_HOLDER: 'leftCardHolder',
                SETTINGS_LINK: 'settingsLink',
                CAPTCHA_INPUT: 'altcha',
                OTHER_INPUTS: 'otherInputs',
                SETTINGS_ICON: 'settingsIcon',

                SETTINGS_CATEGORY_CLASS: 'settings-category',
                SETTING_ITEM_CLASS: 'setting-item',
                SETTING_ITEM_CONTROL_CLASS: 'setting-item-control',
                KEYBIND_INPUT_CLASS: 'keybind-input',
                RESET_SETTING_BTN_CLASS: 'reset-setting-btn',
                RESET_ALL_BUTTON_CLASS: 'reset-all-button',
            },
            TEXT: {
                MOD_SETTINGS_HEADER: 'Gameplay Settings',
                KEYBIND_FOCUS_TEXT: '...',
                RESET_ALL_CONFIRM: 'Are you sure you want to reset all mod settings to their defaults? The page will reload.',
                RESET_ALL_BUTTON_TEXT: 'Rexset All Settings',
                RESET_BUTTON_TITLE: 'Reset to default',
                RESET_BUTTON_TEXT: 'Reset',
            },
        },

        /** @property {object} state - Dynamic state for this minimod. */
        state: {
            /** @property {object} savedSettings - Settings loaded from localStorage. */
            savedSettings: {},
            /** @property {object} defaultSettings - A temporary store of default settings for the reset feature. */
            defaultSettings: {},
        },

        /** @property {object} config - Holds user-configurable settings. */
        config: {
            /** @property {boolean} isPanelVisible - Toggle whether or not to show/hide the panel. */
            isPanelVisible: true,
        },

        // --- MINI-MOD LIFECYCLE & HOOKS ---

        /**
         * Initializes the settings panel creation.
         * @returns {void}
         */
        init() {
            this.loadSettings();
            this.createAndInjectSettingsCard();

            this.applySettingsToAllMods();
        },

        /**
         * Returns the CSS rules required for styling the settings panel.
         * @returns {string} The complete CSS string.
         */
        applyCSS() {
            const LocalC = this.constants;
            const CoreC = this.core.data.constants;

            return `
                #${LocalC.DOM.LEFT_CARD_HOLDER} {
                    display: inline-block;
                    vertical-align: top;
                }

                :is(#${LocalC.DOM.LEFT_CARD_HOLDER}, #${CoreC.DOM.RIGHT_CARD_HOLDER}) > .${CoreC.DOM.MENU_CARD_CLASS} {
                    min-height: 250px;
                }

                #${LocalC.DOM.MOD_CARD} {
                    max-height: 250px;
                    overflow-y: auto;
                    -webkit-overflow-scrolling: touch;
                }

                #${LocalC.DOM.MOD_CARD} > .menuHeader {
                    margin-bottom: 15px;    
                }

                #${LocalC.DOM.MOD_CARD} .${LocalC.DOM.SETTINGS_CATEGORY_CLASS} {
                    margin-bottom: 20px;

                    & .menuHeader {
                        font-size: 20px;
                        color: #4a4a4a;
                    }
                }

                #${LocalC.DOM.MOD_CARD} .${LocalC.DOM.SETTING_ITEM_CLASS} {
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    margin-bottom: 10px;
                    font-size: 18px; /* Matched to game's .settingRadio */
                    color: #a8a8a8;
                }
                
                #${LocalC.DOM.MOD_CARD} .${LocalC.DOM.SETTING_ITEM_CONTROL_CLASS} {
                    display: flex;
                    align-items: center;
                    gap: 8px; /* Space between input and reset button */
                }

                #${LocalC.DOM.MOD_CARD} .${LocalC.DOM.SETTING_ITEM_CLASS} label {
                    line-height: 1.2;
                    width: 180px;
                    color: #777777;
                    
                    & small {
                        display: block;
                        font-size: 14px;
                        color: #c0c0c0;
                    }
                }

                /* Match game's native input look */
                #${LocalC.DOM.MOD_CARD} input[type="text"],
                #${LocalC.DOM.MOD_CARD} input[type="number"] {
                    text-align: center;
                    font-size: 14px;
                    padding: 4px;
                    border: none;
                    outline: none;
                    box-sizing: border-box;
                    color: #4A4A4A;
                    background-color: #e5e3e3;
                    width: calc(8px + 3ch + 20px);
                    border-radius: 4px;

                    &[type="number"] {
                        text-align: start;
                        padding-left: 8px;
                    }
                }
                    
                #${LocalC.DOM.MOD_CARD} .${LocalC.DOM.KEYBIND_INPUT_CLASS} {
                    cursor: pointer;
                    text-transform: uppercase;

                    &:focus {
                        background-color: #d0d0d0;
                    }
                }
                
                #${LocalC.DOM.MOD_CARD} .${LocalC.DOM.RESET_SETTING_BTN_CLASS} {
                    font-size: 14px;
                    color: #d0635c;
                    cursor: pointer;

                    &:hover {
                        color: #984742;
                        text-decoration: underline;
                    }
                }

                /* Button styling to match game's .menuButton */
                #${LocalC.DOM.MOD_CARD} .${LocalC.DOM.RESET_ALL_BUTTON_CLASS} {
                    font-size: 18px;
                    padding: 5px;
                    margin-top: 10px;
                    background-color: #f75d59; /* Red for reset/danger */
                    
                    &:hover {
                        background-color: #ea6b64;
                    }
                }

                #${LocalC.DOM.OTHER_INPUTS} {
                    height: 100%;
                    display: flex;
                    align-items: center;
                    gap: 9px;
                    
                    > #${LocalC.DOM.SETTINGS_ICON} {
                        display: block;
                        height: 44px;
                        aspect-ratio: 1 / 1;
                        background: url('https://raw.githubusercontent.com/TimChinye/UserScripts/c76a1b7552434f093774949cfcbf4f57c37b6fdd/MooMoo.io%20Utility%20Mod/settings-icon.svg') center / 100% no-repeat;
                        background-clip: border-box;
                        opacity: 0.5;
                        cursor: pointer;
                        border: 4.5px solid transparent;
                        
                        &:hover {
                            filter: opacity(0.75);
                        }

                        & ~ #altcha {
                            flex: 1;
                            order: -1;
                        }
                    }
                }
            `;
        },
        
        /**
         * Cleans up all UI created by this minimod.
         * @returns {void}
         */
        cleanup() {
            const LocalC = this.constants;

            // Restore the original structure
            this.config.isPanelVisible = false;
            this.removeSettingsCard();

            const rightCardHolder = document.getElementById(LocalC.DOM.RIGHT_CARD_HOLDER);
            if (leftCardHolder) rightCardHolder.querySelector('.menuHeader:has(+ .settingRadio)').textContent = 'Settings';

            // Remove the settings panel card
            const leftCardHolder = document.getElementById(LocalC.DOM.LEFT_CARD_HOLDER);
            if (leftCardHolder) leftCardHolder.remove();

            // Unwrap captcha input
            const captchaElem = document.getElementById(LocalC.DOM.CAPTCHA_INPUT);
            if (captchaElem) {
                captchaElem.parentElement.before(captchaElem);
                captchaElem.parentElement.remove();
            }
        },

        // --- CORE LOGIC ---

        /**
         * Loads settings from localStorage into the state.
         * @returns {void}
         */
        loadSettings() {
            try {
                const saved = localStorage.getItem(this.constants.LOCALSTORAGE_KEY);
                this.state.savedSettings = saved ? JSON.parse(saved) : {};

                Logger.log("Settings loaded from localStorage.", "color: lightblue;");
            } catch (e) {
                Logger.error("Failed to load settings from localStorage.", e);
                this.state.savedSettings = {};
            }
        },

        /**
         * Saves a single setting value to localStorage.
         * @param {string} key - The unique ID of the setting.
         * @param {any} value - The value to save.
         * @returns {void}
         */
        saveSetting(key, value) {
            this.state.savedSettings[key] = value;
            localStorage.setItem(this.constants.LOCALSTORAGE_KEY, JSON.stringify(this.state.savedSettings));
        },

        /**
         * Applies all loaded settings to their respective mods' config objects.
         * @returns {void}
         */
        applySettingsToAllMods() {
            this.state.defaultSettings = {}; // Clear previous defaults

            const allMods = this.core.miniMods;
            allMods.forEach(mod => {
                if (!mod.getSettings) return;
                const configObj = mod.name === 'Core' ? this.core.config : mod.config;

                mod.getSettings().forEach(setting => {
                    // Store the default value before creating the input
                    this.state.defaultSettings[setting.id] = mod.config[setting.configKey];

                    const savedValue = this.state.savedSettings[setting.id];
                    if (savedValue !== undefined) {
                        configObj[setting.configKey] = savedValue;
                    }
                });
            });
            Logger.log("Applied saved settings to all modules.");
        },

        /**
         * Clears all mod settings from localStorage and reloads the page.
         * @returns {void}
         */
        resetAllSettings() {
            localStorage.removeItem(this.constants.LOCALSTORAGE_KEY);
            this.state.savedSettings = {};
            // Reload the page to apply all defaults cleanly
            window.location.reload();
        },

        // --- UI GENERATION ---

        /**
         * Rearranges the main menu to create and inject the settings card.
         * @returns {Promise<void>}
         */
        createAndInjectSettingsCard() {
            const CoreC = this.core.data.constants;
            const LocalC = this.constants;

            const updatePanelVisibility = () => {
                if (this.config.isPanelVisible) this.showSettingsCard();
                else this.removeSettingsCard();
            }

            return this.core.waitForElementsToLoad(CoreC.DOM.MENU_CARD_HOLDER).then((menuCardHolder) => {
                const settingLabel = 'settings_panel_visible';

                // Set panel visibility to it's previous state, using localStorage.
                const panelVisibility = this.state.savedSettings[settingLabel];
                if (typeof panelVisibility === 'boolean') this.config.isPanelVisible = panelVisibility;

                const rightCardHolder = menuCardHolder.lastElementChild;
                if (!rightCardHolder) return; // Safety check
                rightCardHolder.querySelector('.menuHeader:has(+ .settingRadio)').textContent = 'Display Settings';

                const leftCardHolder = rightCardHolder.cloneNode(true);
                leftCardHolder.id = LocalC.DOM.LEFT_CARD_HOLDER;

                const modCard = leftCardHolder.firstElementChild;
                modCard.id = LocalC.DOM.MOD_CARD;
                modCard.innerHTML = ''; // Clear the cloned content

                menuCardHolder.className = Date.now();
                menuCardHolder.prepend(leftCardHolder);

                // Now that panel has been injected into the page, toggle visibility.
                updatePanelVisibility();

                // Now that the card exists, populate it with the settings.
                this.populateSettingsPanel(modCard);

                const captchaElem = document.getElementById(LocalC.DOM.CAPTCHA_INPUT);
                captchaElem.insertAdjacentHTML('beforebegin', `<div id="${LocalC.DOM.OTHER_INPUTS}"><button id="${LocalC.DOM.SETTINGS_ICON}"></button></div>`);
                document.getElementById(LocalC.DOM.OTHER_INPUTS).append(captchaElem);
                document.getElementById(LocalC.DOM.SETTINGS_ICON).addEventListener('click', () => {
                    this.config.isPanelVisible = !this.config.isPanelVisible;

                    updatePanelVisibility();

                    this.saveSetting(settingLabel, this.config.isPanelVisible); // Update localStorage.
                });
            });
        },

        /**
         * Removes the settings card from the DOM.
         * @returns {void}
         */
        showSettingsCard() {
            const CoreC = this.core.data.constants;
            const LocalC = this.constants;

            const leftCardHolder = document.getElementById(LocalC.DOM.LEFT_CARD_HOLDER);
            if (leftCardHolder) leftCardHolder.style.removeProperty('display');

            const menuCardHolder = document.getElementById(CoreC.DOM.MENU_CARD_HOLDER);
            const promoImgHolder = document.getElementById(CoreC.DOM.AD_HOLDER);
            const wideAdCard = document.getElementById(CoreC.DOM.WIDE_AD_CARD);
            const adCard = document.getElementById(CoreC.DOM.AD_CARD);

            if (menuCardHolder.previousElementSibling !== wideAdCard) menuCardHolder.before(wideAdCard);
            if (promoImgHolder.lastElementChild !== adCard) promoImgHolder.append(adCard);
        },

        /**
         * Removes the settings card from the DOM.
         * @returns {void}
         */
        removeSettingsCard() {
            const CoreC = this.core.data.constants;
            const LocalC = this.constants;

            const leftCardHolder = document.getElementById(LocalC.DOM.LEFT_CARD_HOLDER);
            if (leftCardHolder) leftCardHolder.style.setProperty('display', 'none');

            const menuCardHolder = document.getElementById(CoreC.DOM.MENU_CARD_HOLDER);
            const promoImgHolder = document.getElementById(CoreC.DOM.AD_HOLDER);
            const wideAdCard = document.getElementById(CoreC.DOM.WIDE_AD_CARD);
            const adCard = document.getElementById(CoreC.DOM.AD_CARD);

            if (menuCardHolder.nextElementSibling !== wideAdCard) menuCardHolder.after(wideAdCard);
            if (menuCardHolder.firstElementChild !== adCard) menuCardHolder.prepend(adCard);
        },

        /**
         * Fills the settings panel with inputs for all registered mods.
         * @param {HTMLElement} panel - The 'modCard' element to fill.
         * @returns {void}
         */
        populateSettingsPanel(panel) {
            const LocalC = this.constants;
            panel.innerHTML = `<div class="menuHeader">${LocalC.TEXT.MOD_SETTINGS_HEADER}</div>`;

            const allMods = this.core.miniMods;

            allMods.forEach(mod => {
                if (!mod.getSettings) return;

                const settings = mod.getSettings();
                if (settings.length === 0) return;

                const categoryDiv = document.createElement('div');
                categoryDiv.className = LocalC.DOM.SETTINGS_CATEGORY_CLASS;
                categoryDiv.innerHTML = `<div class="menuHeader">${mod.name}</div>`;
                panel.append(categoryDiv);

                settings.forEach(setting => {
                    const itemDiv = this.createSettingInput(setting, mod.config, mod);
                    categoryDiv.append(itemDiv);
                });
            });

            const itemDiv = document.createElement('div');
            itemDiv.className = `menuButton ${LocalC.DOM.RESET_ALL_BUTTON_CLASS}`;
            itemDiv.textContent = LocalC.TEXT.RESET_ALL_BUTTON_TEXT;
            itemDiv.addEventListener('click', () => {
                if (window.confirm(LocalC.TEXT.RESET_ALL_CONFIRM)) {
                    this.resetAllSettings();
                }
            });

            panel.append(itemDiv);
        },

        /**
         * Creates a single HTML input element for a given setting definition.
         * @param {object} setting - The setting definition object.
         * @param {object} config - The config object of the mod the setting belongs to.
         * @param {object} mod - The mod object itself.
         * @returns {HTMLElement} The generated setting item element.
         */
        createSettingInput(setting, config, mod) {
            const LocalC = this.constants;
            const itemDiv = document.createElement('div');
            itemDiv.className = LocalC.DOM.SETTING_ITEM_CLASS;
            let currentValue = config[setting.configKey];

            let controlHtml = '';
            switch (setting.type) {
                case 'checkbox':
                    controlHtml = `<input type="checkbox" id="${setting.id}" ${currentValue ? 'checked' : ''}>`;
                    break;
                case 'keybind':
                    controlHtml = `<input type="text" class="${LocalC.DOM.KEYBIND_INPUT_CLASS}" id="${setting.id}" value="${currentValue}" readonly>`;
                    break;
                case 'number':
                    controlHtml = `<input type="number" id="${setting.id}" value="${currentValue}" min="${setting.min || 0}" max="${setting.max || 10000}" step="${setting.step || 1}">`;
                    break;
            }

            // Add a reset button for settings that are not buttons
            const resetButtonHtml = `<div class="${LocalC.DOM.RESET_SETTING_BTN_CLASS}" title="${LocalC.TEXT.RESET_BUTTON_TITLE}">${LocalC.TEXT.RESET_BUTTON_TEXT}</div>`;

            itemDiv.innerHTML = `
                <label for="${setting.id}">
                    ${setting.label}
                    ${setting.desc ? `<small>${setting.desc}</small>` : ''}
                </label>
                <div class="${LocalC.DOM.SETTING_ITEM_CONTROL_CLASS}">
                    ${controlHtml}
                    ${resetButtonHtml}
                </div>
            `;

            const input = itemDiv.querySelector('input');
            const resetButton = itemDiv.querySelector(`.${LocalC.DOM.RESET_SETTING_BTN_CLASS}`);

            const updateSetting = (newValue) => {
                // Update the live config object
                config[setting.configKey] = newValue;

                // Save to local storage if it's not a temporary setting
                if (setting.save !== false) {
                    this.saveSetting(setting.id, newValue);
                }

                // Trigger any immediate callback
                if (setting.onChange) {
                    setting.onChange(newValue, this.core);
                }
            };

            resetButton.addEventListener('click', () => {
                const defaultValue = this.state.defaultSettings[setting.id];
                if (setting.type === 'checkbox') {
                    input.checked = defaultValue;
                } else {
                    input.value = defaultValue;
                }
                updateSetting(defaultValue);
            });

            if (setting.type === 'keybind') {
                input.addEventListener('focus', () => input.value = LocalC.TEXT.KEYBIND_FOCUS_TEXT);
                input.addEventListener('blur', () => input.value = config[setting.configKey]);
                input.addEventListener('keydown', e => {
                    e.preventDefault();
                    const key = e.key.toUpperCase();
                    input.value = key;
                    updateSetting(key);
                    input.blur();
                });
            } else {
                input.addEventListener('change', () => {
                    const value = setting.type === 'checkbox' ? input.checked : (setting.type === 'number' ? Number(input.value) : input.value);
                    updateSetting(value);
                });
            }
            return itemDiv;
        },
    };

    /**
     * @module ScrollInventoryMiniMod
     * @description A minimod for Minecraft-style inventory selection with the scroll wheel and hotkeys.
     */
    const ScrollInventoryMiniMod = {

        // --- MINI-MOD PROPERTIES ---

        /** @property {object|null} core - A reference to the core module. */
        core: null,

        /** @property {string} name - The display name of the minimod. */
        name: "Scrollable Inventory",

        /** @property {object} config - Holds user-configurable settings. */
        config: {
            /** @property {boolean} enabled - Master switch for this minimod. */
            enabled: true,
            /** @property {boolean} INVERT_SCROLL - If true, reverses the scroll direction for item selection. */
            INVERT_SCROLL: false
        },

        /** @property {object} constants - Constants specific to this minimod. */
        constants: {
            HOTKEYS: {
                USE_FOOD: 'Q',
            },
            CSS: {
                FILTER_EQUIPPABLE: 'grayscale(0) brightness(1)',
                FILTER_UNEQUIPPABLE: 'grayscale(1) brightness(0.75)',
                BORDER_NONE: 'none',
                SELECTION_BORDER_STYLE: '2px solid white',
            },
        },

        /** @property {object} state - Dynamic state for this minimod. */
        state: {
            /** @property {number} selectedItemIndex - The current index within the filtered list of *equippable* items. */
            selectedItemIndex: -1,
            /** @property {number} lastSelectedWeaponIndex - The index of the last selected weapon, used to auto-switch back after using a non-weapon item. */
            lastSelectedWeaponIndex: -1,
            /** @property {object} boundHandlers - Stores bound event handler functions for easy addition and removal of listeners. */
            boundHandlers: {},
        },

        /**
         * Defines the settings for this minimod.
         * @returns {Array<object>} An array of setting definition objects.
         */
        getSettings() {
            return [
                {
                    id: 'scroll_inv_enabled',
                    configKey: 'enabled',
                    label: 'Enable Scroll Inventory',
                    desc: 'Use scroll wheel to cycle through items.',
                    type: 'checkbox'
                },
                {
                    id: 'scroll_inv_invert',
                    configKey: 'INVERT_SCROLL',
                    label: 'Invert Scroll Direction',
                    desc: 'Changes which way the selection moves.',
                    type: 'checkbox'
                }
            ];
        },

        // --- MINI-MOD LIFECYCLE & HOOKS ---

        /**
         * Handles incoming game packets to update the minimod's state.
         * @param {string} packetName - The human-readable name of the packet.
         * @param {object} packetData - The parsed data object from the packet.
         * @returns {void}
         */
        onPacket(packetName, packetData) {
            if (!this.config.enabled) return;

            switch (packetName) {
                case 'Setup Game': {
                    // Stores the client's player ID upon initial connection.
                    this.core.state.playerId = packetData.yourSID;
                    break;
                }

                case 'Add Player': {
                    // When the client player spawns, trigger the core's onGameReady to finalize setup.
                    if (this.core.state.playerId === packetData.sid && packetData.isYou) {
                        this.core.onGameReady();
                    }
                    break;
                }

                case 'Update Player Value': {
                    // Updates player resource counts and refreshes equippable item states.
                    // If a non-gold resource decreases, assume item usage and try to revert to the last selected weapon.
                    const resourceType = packetData.propertyName;
                    const oldAmount = this.core.state.playerResources[resourceType];
                    this.core.state.playerResources[resourceType] = packetData.value;

                    if (resourceType !== 'points' && packetData.value < oldAmount) {
                        this.state.selectedItemIndex = this.state.lastSelectedWeaponIndex;
                    }

                    this.refreshEquippableState();
                    break;
                }

                case 'Update Item Counts': {
                    // Updates the count of placed items (e.g; walls, traps) and refreshes equippable states.
                    // This is crucial for enforcing placement limits.
                    const itemData = this.core.data._itemDataByServerId.get(packetData.groupID);
                    if (itemData && itemData.limitGroup) {
                        this.core.state.playerPlacedItemCounts.set(itemData.limitGroup, packetData.count);
                        this.refreshEquippableState();
                    }
                    break;
                }

                case 'Update Upgrades': {
                    this.refreshEquippableState();
                    break;
                }
            }
        },

        /**
         * Adds all necessary event listeners for this minimod.
         * @returns {void}
         */
        addEventListeners() {
            if (!this.config.enabled) return;

            const CoreC = this.core.data.constants;

            this.state.boundHandlers.handleInventoryScroll = this.handleInventoryScroll.bind(this);
            this.state.boundHandlers.handleKeyPress = this.handleKeyPress.bind(this);
            this.state.boundHandlers.handleItemClick = this.handleItemClick.bind(this);

            document.addEventListener('wheel', this.state.boundHandlers.handleInventoryScroll, { passive: false });
            document.addEventListener('keydown', this.state.boundHandlers.handleKeyPress);
            document.getElementById(CoreC.DOM.ACTION_BAR).addEventListener('click', this.state.boundHandlers.handleItemClick);
        },

        /**
         * Cleans up by removing all event listeners added by this minimod.
         * @returns {void}
         */
        cleanup() {
            const CoreC = this.core.data.constants;
            document.removeEventListener('wheel', this.state.boundHandlers.handleInventoryScroll);
            document.removeEventListener('keydown', this.state.boundHandlers.handleKeyPress);
            const actionBar = document.getElementById(CoreC.DOM.ACTION_BAR);
            if (actionBar) {
                actionBar.removeEventListener('click', this.state.boundHandlers.handleItemClick);
            }
        },

        /**
         * Called when the player has spawned. Scrapes initial state and sets up UI.
         * @returns {void}
         */
        onGameReady() {
            if (!this.config.enabled) return;

            const CoreC = this.core.data.constants;

            // Wait for Game UI to load before proceeding
            const gameUI = document.getElementById(CoreC.DOM.GAME_UI);
            this.core.waitForVisible(gameUI).then(() => {
                // Scrape initial state from the DOM
                const resElements = document.getElementById(CoreC.DOM.RESOURCE_DISPLAY).children;
                this.core.state.playerResources = {
                    food: parseInt(resElements[0].textContent) || 0,
                    wood: parseInt(resElements[1].textContent) || 0,
                    stone: parseInt(resElements[2].textContent) || 0,
                    gold: parseInt(resElements[3].textContent) || 0,
                };

                // Set the initial selected item
                this.selectItemByIndex(CoreC.GAME_STATE.INITIAL_SELECTED_ITEM_INDEX);
            });
        },

        // --- EVENT HANDLERS ---

        /**
         * Handles the 'wheel' event to cycle through inventory items.
         * @param {WheelEvent} event - The DOM wheel event.
         * @returns {void}
         */
        handleInventoryScroll(event) {
            if (!this.config.enabled) return;

            const CoreC = this.core.data.constants;
            if (this.core.isInputFocused() || !this.core.state.gameSocket || this.core.state.gameSocket.readyState !== CoreC.GAME_STATE.WEBSOCKET_STATE_OPEN) return;

            // Determine scroll direction and send to refresh selection UI function.
            let scrollDirection = event.deltaY > 0 ? CoreC.GAME_STATE.SCROLL_DOWN : CoreC.GAME_STATE.SCROLL_UP;
            if (this.config.INVERT_SCROLL) { scrollDirection *= -1; }
            this.refreshEquippableState(scrollDirection);
        },

        /**
         * Handles keyboard shortcuts for direct item selection.
         * @param {KeyboardEvent} event - The DOM keyboard event.
         * @returns {void}
         */
        handleKeyPress(event) {
            if (!this.config.enabled) return;

            const CoreC = this.core.data.constants;

            if (this.core.isInputFocused()) return;

            const pressedKey = event.key.toUpperCase();
            const actionBar = document.getElementById(CoreC.DOM.ACTION_BAR);
            if (!actionBar) return;

            const availableItems = Array.from(actionBar.children).filter(el => this.core.isAvailableItem(el));
            if (availableItems.length === 0) return;

            const isNumericHotkey = (key) => key >= '1' && key <= '9';
            const isFoodHotkey = (key) => key === this.constants.HOTKEYS.USE_FOOD;
            const findFoodItem = (items) => items.find(el => this.core.getItemFromElem(el)?.itemType === CoreC.ITEM_TYPES.FOOD);

            let targetElement = null;
            if (isNumericHotkey(pressedKey)) {
                targetElement = availableItems[parseInt(pressedKey) - 1];
            } else if (isFoodHotkey(pressedKey)) {
                targetElement = findFoodItem(availableItems);
            }

            if (targetElement && this.core.isEquippableItem(targetElement)) {
                const equippableItems = Array.from(actionBar.children).filter(el => this.core.isEquippableItem(el));
                const newIndex = equippableItems.findIndex(el => el.id === targetElement.id);
                if (newIndex !== -1) {
                    if (this.state.selectedItemIndex === newIndex) {
                        // Switch to weapon instead.
                        this.selectItemByIndex(this.state.lastSelectedWeaponIndex);
                    }
                    else this.selectItemByIndex(newIndex);
                }
            }
        },

        /**
         * Handles direct item selection by clicking on an item in the action bar.
         * @param {MouseEvent} event - The DOM mouse event.
         * @returns {void}
         */
        handleItemClick(event) {
            if (!this.config.enabled) return;

            if (this.core.isInputFocused()) return;
            const CoreC = this.core.data.constants;
            const clickedElement = event.target.closest(CoreC.DOM.ACTION_BAR_ITEM_CLASS);
            if (clickedElement && this.core.isEquippableItem(clickedElement)) {
                const actionBar = document.getElementById(CoreC.DOM.ACTION_BAR);
                if (!actionBar) return;
                const equippableItems = Array.from(actionBar.children).filter(el => this.core.isEquippableItem(el));
                const newIndex = equippableItems.findIndex(el => el.id === clickedElement.id);
                if (newIndex !== -1) this.selectItemByIndex(newIndex);
            }
        },

        // --- CORE LOGIC & UI MANIPULATION ---

        /**
         * The master function for refreshing the inventory selection state and UI. It recalculates the list
         * of equippable items, determines the new selection index, sends an equip packet if needed, and updates the UI.
         * @param {number} [scrollDirection=0] - The direction of scroll: 1 for down, -1 for up, 0 for no change.
         * @returns {void}
         */
        refreshEquippableState(scrollDirection = this.core.data.constants.GAME_STATE.NO_SCROLL) {
            const CoreC = this.core.data.constants;
            const actionBar = document.getElementById(CoreC.DOM.ACTION_BAR);
            if (!actionBar) return;

            const equippableItems = Array.from(actionBar.children).filter(el => this.core.isEquippableItem(el));
            if (equippableItems.length === 0) {
                Logger.warn("No equippable items available.");
                this.state.selectedItemIndex = -1;
                this.updateSelectionUI(null);
                return;
            }

            // Calculate new index, handling scrolling and list changes.
            this.state.selectedItemIndex = (this.state.selectedItemIndex + scrollDirection + equippableItems.length) % equippableItems.length;

            // Store the last active weapon's index.
            if (equippableItems[1]) {
                const secondEquippableItem = this.core.getItemFromElem(equippableItems[1]);
                if (this.state.selectedItemIndex <= CoreC.ITEM_TYPES.SECONDARY_WEAPON) {
                    const isSingleWielder = secondEquippableItem?.itemType > CoreC.ITEM_TYPES.SECONDARY_WEAPON;
                    this.state.lastSelectedWeaponIndex = isSingleWielder ? CoreC.ITEM_TYPES.PRIMARY_WEAPON : this.state.selectedItemIndex;
                }
            }

            const selectedElement = equippableItems[this.state.selectedItemIndex];
            if (!selectedElement) return;

            // If we scrolled, send the equip packet.
            if (scrollDirection !== CoreC.GAME_STATE.NO_SCROLL) {
                const itemToEquip = this.core.getItemFromElem(selectedElement);
                if (itemToEquip) {
                    const isWeapon = itemToEquip.itemType <= CoreC.ITEM_TYPES.SECONDARY_WEAPON;
                    this.core.sendGamePacket(CoreC.PACKET_TYPES.EQUIP_ITEM, [itemToEquip.id, isWeapon]);
                }
            }

            this.updateSelectionUI(selectedElement);
        },

        /**
         * Selects an item by its index in the list of equippable items.
         * @param {number} newIndex - The index of the item to select.
         * @returns {void}
         */
        selectItemByIndex(newIndex) {
            const CoreC = this.core.data.constants;
            const actionBar = document.getElementById(CoreC.DOM.ACTION_BAR);
            if (!actionBar) return;

            const equippableItems = Array.from(actionBar.children).filter(el => this.core.isEquippableItem(el));
            if (newIndex < 0 || newIndex >= equippableItems.length) return;

            this.state.selectedItemIndex = newIndex;
            this.refreshEquippableState(CoreC.GAME_STATE.NO_SCROLL);
        },

        // --- UI & HELPER FUNCTIONS ---

        /**
         * Updates the action bar UI to highlight the selected item.
         * @param {HTMLElement|null} selectedItem - The element to highlight as selected.
         * @returns {void}
         */
        updateSelectionUI(selectedItem) {
            const CoreC = this.core.data.constants;
            const LocalC = this.constants;

            const actionBar = document.getElementById(CoreC.DOM.ACTION_BAR);
            if (!actionBar) return;

            const allItems = Array.from(actionBar.children);
            allItems.forEach(item => {
                item.style.border = item === selectedItem ? LocalC.CSS.SELECTION_BORDER_STYLE : LocalC.CSS.BORDER_NONE;
                item.style.filter = this.core.isEquippableItem(item) ? LocalC.CSS.FILTER_EQUIPPABLE : LocalC.CSS.FILTER_UNEQUIPPABLE;
            });
        }
    };

    /**
     * @module WearablesToolbarMiniMod
     * @description A minimod that adds a clickable, draggable hotbar for equipping wearables.
     */
    const WearablesToolbarMiniMod = {

        // --- MINI-MOD PROPERTIES ---

        /** @property {object|null} core - A reference to the core module. */
        core: null,

        /** @property {string} name - The display name of the minimod. */
        name: "Wearables Toolbar",
        /** @property {object} config - Holds user-configurable settings. */
        config: {
            /** @property {boolean} enabled - Master switch for this minimod. */
            enabled: true,
            /** @property {string} TOGGLE_KEY - The hotkey to show or hide the toolbar. */
            TOGGLE_KEY: 'P',
            /** @property {boolean} START_HIDDEN - If true, the toolbar will be hidden on spawn. */
            START_HIDDEN: false,
            /** @property {boolean} INSTA_PIN_FREE_HATS - If true, all owned wearables are pinned at the start. */
            INSTA_PIN_FREE_HATS: false,
            /** @property {boolean} DRAGGABLE_ENABLED - If true, wearables can be reordered via drag-and-drop. */
            DRAGGABLE_ENABLED: true,
            /** @property {boolean} AUTO_PIN_ON_BUY - If true, automatically pins a wearable upon purchase. */
            AUTO_PIN_ON_BUY: false,
        },

        /** @property {object} constants - Constants specific to this minimod. */
        constants: {
            TEXT: {
                PIN: 'Pin',
                UNPIN: 'Unpin',
                EQUIP_BUTTON_TEXT: 'equip',
            },
            DOM: {
                WEARABLES_TOOLBAR: 'wearablesToolbar',
                WEARABLES_HOTKEY: 'wearablesHotkey',
                WEARABLES_HATS: 'wearablesHats',
                WEARABLES_ACCESSORIES: 'wearablesAccessories',
                WEARABLES_GRID_CLASS: 'wearables-grid',
                WEARABLE_BUTTON_CLASS: 'wearable-btn',
                WEARABLE_BUTTON_ID_PREFIX: 'wearable-btn-',
                JOIN_ALLIANCE_BUTTON_CLASS: 'joinAlBtn',
                PIN_BUTTON_CLASS: 'pinBtn',
                WEARABLE_SELECTED_CLASS: 'selected',
                WEARABLE_DRAGGING_CLASS: 'dragging'
            },
            CSS: {
                DRAGGING_OPACITY: '0.5',
                STORE_MENU_TRANSFORM: 'translateY(0px)',
            },
            REGEX: {
                HAT_IMG: /hat_(\d+)\.png/,
                ACCESSORY_IMG: /access_(\d+)\.png/,
            },
            URLS: {
                BASE_IMG: 'https://moomoo.io/img',
                HAT_IMG_PATH: '/hats/hat_',
                ACCESSORY_IMG_PATH: '/accessories/access_',
                IMG_EXT: '.png',
            },
            TIMEOUTS: {
                DRAG_AND_DROP_VISIBILITY: 0,
            },
        },

        /** @property {object} state - Dynamic state for this minimod. */
        state: {
            /** @property {boolean} isVisible - Whether the toolbar UI is currently shown. */
            isVisible: true,
            /** @property {Set<number>} pinnedWearables - A set of wearable IDs that the user has pinned to the toolbar. */
            pinnedWearables: new Set(),
            /** @property {HTMLElement|null} draggedItem - The wearable button element currently being dragged. */
            draggedItem: null,
            /** @property {object} boundHandlers - Stores bound event handler functions for easy addition and removal of listeners. */
            boundHandlers: {},
            /** @property {Array<MutationObserver|ResizeObserver>} observers - Stores observers for dynamic UI adjustments and cleanup. */
            observers: []
        },

        /**
         * Defines the settings for this minimod.
         * @returns {Array<object>} An array of setting definition objects.
         */
        getSettings() {
            return [
                {
                    id: 'wearables_toolbar_enabled',
                    configKey: 'enabled',
                    label: 'Enable Wearables Toolbar',
                    desc: 'Adds a draggable hotbar for accessories.',
                    type: 'checkbox',
                    onChange: (value) => this.toggleFeature(value)
                },
                {
                    id: 'wearables_toolbar_toggle_key',
                    configKey: 'TOGGLE_KEY',
                    label: 'Toggle Toolbar Key',
                    desc: 'Press to show or hide the toolbar.',
                    type: 'keybind',
                    onChange: (value) => {
                        const wearableHotkeyLabel = document.getElementById(this.constants.DOM.WEARABLES_HOTKEY);
                        if (wearableHotkeyLabel) wearableHotkeyLabel.textContent = `(Press '${value}' to toggle)`;
                    }
                },
                {
                    id: 'wearables_toolbar_start_hidden',
                    configKey: 'START_HIDDEN',
                    label: 'Start Hidden',
                    desc: 'The toolbar will be hidden on spawn.',
                    type: 'checkbox'
                },
                {
                    id: 'wearables_toolbar_insta_pin_free_hats',
                    configKey: 'INSTA_PIN_FREE_HATS',
                    label: 'Insta-pin Free Hats',
                    desc: 'Automatically pins all the free hats, as soon as the game start.',
                    type: 'checkbox'
                },
                {
                    id: 'wearables_toolbar_draggable',
                    configKey: 'DRAGGABLE_ENABLED',
                    label: 'Draggable Wearables',
                    desc: 'Allow reordering wearables by dragging.',
                    type: 'checkbox',
                    onChange: (value) => this.toggleDraggable(value)
                },
                {
                    id: 'wearables_toolbar_auto_pin_on_buy',
                    configKey: 'AUTO_PIN_ON_BUY',
                    label: 'Auto-pin on Buy',
                    desc: 'Automatically pin a wearable when you buy it.',
                    type: 'checkbox'
                }
            ];
        },

        // --- MINI-MOD LIFECYCLE & HOOKS ---

        /**
         * Returns the CSS rules required for this minimod.
         * @returns {string} The complete CSS string.
         */
        applyCSS() {
            const CoreC = this.core.data.constants;
            const LocalC = this.constants;
            return `
                #${CoreC.DOM.STORE_MENU} {
                    top: 20px;
                    height: calc(100% - 240px);
                    --extended-width: 80px;

                    & .${CoreC.DOM.STORE_TAB_CLASS} {
                        padding: 10px calc(10px + (var(--extended-width) / 4));
                    }

                    & #${CoreC.DOM.STORE_HOLDER} {
                        height: 100%;
                        width: calc(400px + var(--extended-width));
                    }

                    &.${CoreC.DOM.STORE_MENU_EXPANDED_CLASS} {
                        top: 140px;
                        height: calc(100% - 360px);
                    }
                }

                .${LocalC.DOM.PIN_BUTTON_CLASS} {
                    --text-color: hsl(from #80eefc calc(h + 215) s l);
                    color: var(--text-color);
                    padding-right: 5px;

                    &:hover {
                        color: hsl(from var(--text-color) h calc(s * 0.5) calc(l * 0.875));
                    }
                }

                #${CoreC.DOM.ITEM_INFO_HOLDER} {
                    top: calc(20px + var(--top-offset, 0px));
                }

                #${LocalC.DOM.WEARABLES_TOOLBAR} {
                    position: absolute;
                    left: 20px;
                    top: 20px;
                    padding: 7px 10px 5px;
                    width: auto;
                    max-width: 440px;
                    background-color: rgba(0, 0, 0, 0.25);
                    border-radius: 3px;
                    pointer-events: all;
                    
                    & > h1 {
                        margin: 0;
                        color: #fff;
                        font-size: 31px;
                        font-weight: inherit;

                        & > span {
                            font-size: 0.5em;
                            vertical-align: middle;
                        }
                    }
                }

                .${LocalC.DOM.WEARABLES_GRID_CLASS} {
                    display: flex;
                    flex-wrap: wrap;
                    gap: 5px;
                    justify-content: flex-start;
                }

                .${LocalC.DOM.WEARABLE_BUTTON_CLASS} {
                    width: 40px;
                    height: 40px;
                    margin: 4px 0;
                    border: 2px solid rgba(255, 255, 255, 0.25);
                    background-size: contain;
                    background-position: center;
                    background-repeat: no-repeat;
                    cursor: pointer;
                    background-color: rgba(0, 0, 0, 0.125);
                    border-radius: 4px;
                    transition: all 0.2s ease;

                    &:hover {
                        background-color: rgba(255, 255, 255, 0.125);
                        border-color: white;
                    }

                    &.${LocalC.DOM.WEARABLE_SELECTED_CLASS} {
                        background-color: #5b9c52;
                        border-color: lightgreen;
                        box-shadow: 0 0 8px lightgreen;
                    }

                    &.${LocalC.DOM.WEARABLE_DRAGGING_CLASS} {
                        opacity: ${LocalC.CSS.DRAGGING_OPACITY};
                    }
                }
            `
        },

        /**
         * Handles incoming game packets to update wearables.
         * @param {string} packetName - The human-readable name of the packet.
         * @param {object} packetData - The parsed data object from the packet.
         * @returns {void}
         */
        onPacket(packetName, packetData) {
            if (!this.config.enabled) return;

            const CoreC = this.core.data.constants;
            switch (packetName) {
                case 'Update Store Items': {
                    const { itemID, itemType, action } = packetData;

                    if (action === CoreC.PACKET_DATA.STORE_ACTIONS.ADD_ITEM) {
                        this.addWearableButton(itemID, itemType);

                        // If auto-pin is on, pin the new item.
                        if (this.config.AUTO_PIN_ON_BUY && !this.isWearablePinned(itemID)) {
                            this.togglePin(itemID, itemType);
                        }
                    } else if (action === CoreC.PACKET_DATA.STORE_ACTIONS.UPDATE_EQUIPPED) {
                        this.updateEquippedWearable(itemID, itemType);
                    }

                    break;
                }

                case 'Add Player': {
                    if (packetData.isYou) {
                        this.state.isVisible = !this.config.START_HIDDEN;
                    }

                    break;
                }
            }
        },

        /**
         * Called when the player has spawned. Creates the toolbar UI.
         * @returns {void}
         */
        onGameReady() {
            if (!this.config.enabled) return;

            const CoreC = this.core.data.constants;
            const LocalC = this.constants;
            if (!this.core.state.playerHasRespawned && !document.getElementById(LocalC.DOM.WEARABLES_TOOLBAR)) {
                // Wait for Game UI to load before proceeding
                const gameUI = document.getElementById(CoreC.DOM.GAME_UI);
                this.core.waitForVisible(gameUI).then(() => {
                    this.createWearablesToolbarUI();

                    this.setupDynamicPositioning();
                    this.setupStoreMenuObservers();
                        
                    if (this.config.INSTA_PIN_FREE_HATS) {
                        this.instaPinFreeHats();
                    }
                });
            }
        },

        /**
         * Cleans up all UI elements, observers, and event listeners.
         * @returns {void}
         */
        cleanup() {
            const CoreC = this.core.data.constants;
            const LocalC = this.constants;
            document.getElementById(CoreC.DOM.STORE_MENU)?.style.removeProperty('transform');
            document.getElementById(LocalC.DOM.WEARABLES_TOOLBAR)?.remove();
            document.querySelectorAll(`.${LocalC.DOM.PIN_BUTTON_CLASS}`).forEach((pinElem) => pinElem.remove());
            document.removeEventListener('keydown', this.state.boundHandlers.handleKeyDown);
            this.state.observers.forEach(obs => obs.disconnect());
            this.state.observers.length = 0;
        },

        /**
         * Immediately shows or hides the feature based on the enabled state.
         * @param {boolean} isEnabled - The new enabled state.
         * @returns {void}
         */
        toggleFeature(isEnabled) {
            const toolbar = document.getElementById(this.constants.DOM.WEARABLES_TOOLBAR);
            if (isEnabled) {
                // If enabling, but toolbar doesn't exist, try to create it
                if (!toolbar) this.onGameReady();
                else toolbar.style.display = this.state.isVisible ? 'block' : 'none';
            } else {
                if (toolbar) toolbar.style.display = 'none';
            }
        },

        // --- INITIAL UI SETUP ---

        /**
         * Creates the main HTML structure for the wearables toolbar.
         * @returns {void}
         */
        createWearablesToolbarUI() {
            const LocalC = this.constants;
            const CoreC = this.core.data.constants;

            const container = document.createElement('div');
            container.id = LocalC.DOM.WEARABLES_TOOLBAR;
            container.innerHTML = `
                <h1>Wearables Toolbar <span id="${LocalC.DOM.WEARABLES_HOTKEY}">(Press '${this.config.TOGGLE_KEY}' to toggle)</span></h1>
                <div id="${LocalC.DOM.WEARABLES_HATS}" class="${LocalC.DOM.WEARABLES_GRID_CLASS}"></div>
                <div id="${LocalC.DOM.WEARABLES_ACCESSORIES}" class="${LocalC.DOM.WEARABLES_GRID_CLASS}"></div>
            `;

            // Apply start hidden setting
            container.style.display = this.state.isVisible ? CoreC.CSS.DISPLAY_BLOCK : CoreC.CSS.DISPLAY_NONE;

            document.getElementById(CoreC.DOM.GAME_UI).prepend(container);

            const hatsGrid = container.querySelector(`#${LocalC.DOM.WEARABLES_HATS}`);
            const accessoriesGrid = container.querySelector(`#${LocalC.DOM.WEARABLES_ACCESSORIES}`);

            hatsGrid.addEventListener('dragover', this.handleDragOver.bind(this));
            accessoriesGrid.addEventListener('dragover', this.handleDragOver.bind(this));

            this.state.boundHandlers.handleKeyDown = (e) => {
                if (!this.config.enabled || this.core.isInputFocused()) return;

                if (e.key.toUpperCase() === this.config.TOGGLE_KEY) {
                    this.state.isVisible = !this.state.isVisible;
                    container.style.display = this.state.isVisible ? CoreC.CSS.DISPLAY_BLOCK : CoreC.CSS.DISPLAY_NONE;
                }
            };

            document.addEventListener('keydown', this.state.boundHandlers.handleKeyDown);
        },

        /**
         * Sets up observers to dynamically position the toolbar and info box.
         * @returns {void}
         */
        setupDynamicPositioning() {
            const LocalC = this.constants;
            const CoreC = this.core.data.constants;
            const toolbar = document.getElementById(LocalC.DOM.WEARABLES_TOOLBAR);
            const infoHolder = document.getElementById(CoreC.DOM.ITEM_INFO_HOLDER);

            if (!toolbar || !infoHolder) return Logger.warn("Could not find toolbar or info holder for dynamic positioning.");

            const updatePosition = () => {
                const isExpanded = infoHolder.offsetHeight > 0;
                infoHolder.style.setProperty('--top-offset', isExpanded ? `${toolbar.offsetHeight + 20}px` : '0px');
            };

            // Observer 1: Reacts to any change in the info holder's size (e.g; appearing/disappearing).
            const infoHolderObserver = new ResizeObserver(updatePosition);
            infoHolderObserver.observe(infoHolder);
            this.state.observers.push(infoHolderObserver);

            // Observer 2: Reacts to significant changes in the toolbar's height,
            // which happens when a new row of wearables is pinned.
            let lastToolbarHeight = toolbar.offsetHeight;
            const toolbarObserver = new ResizeObserver(() => {
                const currentHeight = toolbar.offsetHeight;
                // Only update if the height changes by 10px or more to avoid minor fluctuations.
                if (Math.abs(currentHeight - lastToolbarHeight) >= 10) {
                    updatePosition();
                    lastToolbarHeight = currentHeight; // Update the last known height
                }
            });
            toolbarObserver.observe(toolbar);
            this.state.observers.push(toolbarObserver);

            // Run once at the start to set the initial position.
            updatePosition();
        },

        /**
         * Sets up observers to adjust the store menu and inject pin buttons.
         * @returns {void}
         */
        setupStoreMenuObservers() {
            const CoreC = this.core.data.constants;
            const LocalC = this.constants;
            const storeMenu = document.getElementById(CoreC.DOM.STORE_MENU);
            storeMenu.style.transform = LocalC.CSS.STORE_MENU_TRANSFORM;

            const upgradeHolder = document.getElementById(CoreC.DOM.UPGRADE_HOLDER);
            const upgradeCounter = document.getElementById(CoreC.DOM.UPGRADE_COUNTER);

            const initialCheck = () => {
                const upgradeHolderVisible = upgradeHolder.style.display === CoreC.CSS.DISPLAY_BLOCK;
                const upgradeCounterVisible = upgradeCounter.style.display === CoreC.CSS.DISPLAY_BLOCK;

                const isExpanded = upgradeHolderVisible && upgradeCounterVisible;
                storeMenu.classList.toggle(CoreC.DOM.STORE_MENU_EXPANDED_CLASS, isExpanded);
            };

            initialCheck();
            const upgradeObserver = new MutationObserver(initialCheck);
            upgradeObserver.observe(upgradeHolder, { attributes: true, attributeFilter: ['style'] });
            upgradeObserver.observe(upgradeCounter, { attributes: true, attributeFilter: ['style'] });
            this.state.observers.push(upgradeObserver);

            const storeMenuObserver = new MutationObserver((mutationsList) => {
                for (const mutation of mutationsList) {
                    if (storeMenu.style.display === CoreC.CSS.DISPLAY_BLOCK && mutation.oldValue?.includes(`display: ${CoreC.CSS.DISPLAY_NONE}`)) {
                        this.addPinButtons();
                    }
                }
            });
            storeMenuObserver.observe(storeMenu, { attributes: true, attributeFilter: ['style'], attributeOldValue: true });
            this.state.observers.push(storeMenuObserver);

            const storeHolderObserver = new MutationObserver(() => this.addPinButtons());
            storeHolderObserver.observe(document.getElementById(CoreC.DOM.STORE_HOLDER), { childList: true });
            this.state.observers.push(storeHolderObserver);
        },

        // --- UI MANIPULATION & STATE UPDATES ---

        /**
         * Adds a new button for a specific wearable to the toolbar.
         * @param {number} id - The server-side ID of the wearable.
         * @param {string} type - The type of wearable ('hat' or 'accessory').
         * @returns {void}
         */
        addWearableButton(id, type) {
            const LocalC = this.constants;
            const CoreC = this.core.data.constants;
            const containerId = type === CoreC.PACKET_DATA.WEARABLE_TYPES.HAT ? LocalC.DOM.WEARABLES_HATS : LocalC.DOM.WEARABLES_ACCESSORIES;
            const container = document.getElementById(containerId);
            if (!container) return;

            const buttonId = `${LocalC.DOM.WEARABLE_BUTTON_ID_PREFIX}${type}-${id}`;
            if (document.getElementById(buttonId)) return;

            const btn = document.createElement('div');
            btn.id = buttonId;
            btn.className = LocalC.DOM.WEARABLE_BUTTON_CLASS;
            btn.draggable = this.config.DRAGGABLE_ENABLED;
            btn.dataset.wearableId = id;

            const imagePath = type === CoreC.PACKET_DATA.WEARABLE_TYPES.HAT ? LocalC.URLS.HAT_IMG_PATH : LocalC.URLS.ACCESSORY_IMG_PATH;
            btn.style.backgroundImage = `url(${LocalC.URLS.BASE_IMG}${imagePath}${id}${LocalC.URLS.IMG_EXT})`;
            btn.title = `Item ID: ${id}`;

            btn.addEventListener('dragstart', () => {
                this.state.draggedItem = btn;
                setTimeout(() => btn.classList.add(LocalC.DOM.WEARABLE_DRAGGING_CLASS), LocalC.TIMEOUTS.DRAG_AND_DROP_VISIBILITY);
            });

            btn.addEventListener('dragend', () => {
                setTimeout(() => {
                    if (this.state.draggedItem) this.state.draggedItem.classList.remove(LocalC.DOM.WEARABLE_DRAGGING_CLASS);
                    this.state.draggedItem = null;
                }, LocalC.TIMEOUTS.DRAG_AND_DROP_VISIBILITY);
            });

            btn.addEventListener('click', () => {
                const isCurrentlySelected = btn.classList.contains(LocalC.DOM.WEARABLE_SELECTED_CLASS);
                const newItemId = isCurrentlySelected ? 0 : id;
                this.core.sendGamePacket(CoreC.PACKET_TYPES.EQUIP_WEARABLE, [0, newItemId, type === CoreC.PACKET_DATA.WEARABLE_TYPES.HAT ? 0 : 1]);
            });

            container.append(btn);
            this.refreshToolbarVisibility();
        },

        /**
         * Updates the visual state of buttons to highlight the equipped wearable.
         * @param {number} id - The server-side ID of the newly equipped wearable.
         * @param {string} type - The type of wearable.
         * @returns {void}
         */
        updateEquippedWearable(id, type) {
            const LocalC = this.constants;
            const CoreC = this.core.data.constants;
            const containerId = type === CoreC.PACKET_DATA.WEARABLE_TYPES.HAT ? LocalC.DOM.WEARABLES_HATS : LocalC.DOM.WEARABLES_ACCESSORIES;
            const container = document.getElementById(containerId);
            if (!container) return;

            const currentSelected = container.querySelector(`.${LocalC.DOM.WEARABLE_SELECTED_CLASS}`);
            if (currentSelected) currentSelected.classList.remove(LocalC.DOM.WEARABLE_SELECTED_CLASS);

            if (id > 0) {
                const buttonId = `${LocalC.DOM.WEARABLE_BUTTON_ID_PREFIX}${type}-${id}`;

                const newSelectedBtn = document.getElementById(buttonId);
                if (newSelectedBtn) newSelectedBtn.classList.add(LocalC.DOM.WEARABLE_SELECTED_CLASS);
            }
        },

        /**
         * Hides or shows wearable buttons based on the pinned set.
         * @returns {void}
         */
        refreshToolbarVisibility() {
            const LocalC = this.constants;
            const CoreC = this.core.data.constants;

            const allButtons = document.querySelectorAll(`.${LocalC.DOM.WEARABLE_BUTTON_CLASS}`);
            allButtons.forEach(btn => {
                const buttonId = parseInt(btn.dataset.wearableId);
                if (!isNaN(buttonId)) {
                    btn.style.display = this.state.pinnedWearables.has(buttonId) ? CoreC.CSS.DISPLAY_BLOCK : CoreC.CSS.DISPLAY_NONE;
                }
            });
        },

        // --- PINNING LOGIC ---

        /**
         * Scans the store for all owned wearables and pins them if they aren't already.
         * This is triggered by the 'Start With All Pinned' setting.
         * @returns {void}
         */
        instaPinFreeHats() {
            const CoreC = this.core.data.constants;

            const freeHats = [51, 50, 28, 29, 30, 36, 37, 38, 44, 35, 42, 43, 49];

            freeHats.forEach((storeItemId) => {
                if (!this.isWearablePinned(storeItemId)) {
                    this.togglePin(storeItemId, CoreC.PACKET_DATA.WEARABLE_TYPES.HAT);
                }
            });

            this.refreshToolbarVisibility();
        },


        /**
         * Scans the store menu and adds "Pin" buttons to owned wearables.
         * @returns {void}
         */
        addPinButtons() {
            const CoreC = this.core.data.constants;
            const LocalC = this.constants;

            const storeHolder = document.getElementById(CoreC.DOM.STORE_HOLDER);
            Array.from(storeHolder.children).forEach((storeItem) => {
                const wearableIcon = storeItem.querySelector('img');
                const joinBtn = storeItem.querySelector('.' + LocalC.DOM.JOIN_ALLIANCE_BUTTON_CLASS);
                const hasPinButton = storeItem.querySelector(`.${LocalC.DOM.PIN_BUTTON_CLASS}`);
                const isNotEquipButton = !joinBtn || !joinBtn.textContent.toLowerCase().includes(LocalC.TEXT.EQUIP_BUTTON_TEXT);

                // Check if eligible for a new pin button.
                if (!wearableIcon || hasPinButton || isNotEquipButton) return;
                let id, type;
                const hatMatch = wearableIcon.src.match(LocalC.REGEX.HAT_IMG);
                const accMatch = wearableIcon.src.match(LocalC.REGEX.ACCESSORY_IMG);

                if (hatMatch) {
                    id = parseInt(hatMatch[1]);
                    type = CoreC.PACKET_DATA.WEARABLE_TYPES.HAT;
                } else if (accMatch) {
                    id = parseInt(accMatch[1]);
                    type = CoreC.PACKET_DATA.WEARABLE_TYPES.ACCESSORY;
                } else {
                    return; // Not a wearable item
                }

                const pinButton = document.createElement('div');
                pinButton.className = `${LocalC.DOM.JOIN_ALLIANCE_BUTTON_CLASS} ${LocalC.DOM.PIN_BUTTON_CLASS}`;
                pinButton.style.marginTop = '5px';
                pinButton.textContent = this.isWearablePinned(id) ? LocalC.TEXT.UNPIN : LocalC.TEXT.PIN;
                pinButton.addEventListener('click', () => {
                    pinButton.textContent = this.togglePin(id, type) ? LocalC.TEXT.UNPIN : LocalC.TEXT.PIN;
                    this.refreshToolbarVisibility();
                });
                joinBtn.after(pinButton);
            });
        },

        /**
         * Checks if a wearable is currently pinned.
         * @param {number} id - The ID of the wearable.
         * @returns {boolean} True if pinned.
         */
        isWearablePinned(id) {
            return this.state.pinnedWearables.has(id);
        },

        /**
         * Toggles the pinned state of a wearable.
         * @param {number} id - The ID of the wearable.
         * @param {string} type - The type of the wearable.
         * @returns {boolean} The new pinned state.
         */
        togglePin(id, type) {
            const pinned = this.state.pinnedWearables;
            if (pinned.has(id)) { // Unpin
                pinned.delete(id);
                return false;
            } else { // Pin
                pinned.add(id);
                this.addWearableButton(id, type);
                return true;
            }
        },

        /**
         * Toggles the draggable property on all wearable buttons based on the user setting.
         * @param {boolean} isEnabled - Whether dragging should be enabled.
         * @returns {void}
         */
        toggleDraggable(isEnabled) {
            const LocalC = this.constants;
            const allButtons = document.querySelectorAll(`.${LocalC.DOM.WEARABLE_BUTTON_CLASS}`);
            allButtons.forEach(btn => {
                btn.draggable = isEnabled;
            });
        },


        // --- EVENT HANDLERS (DRAG & DROP) ---

        /**
         * Handles the dragover event for reordering pinned items.
         * @param {DragEvent} e - The DOM drag event.
         * @returns {void}
         */
        handleDragOver(e) {
            e.preventDefault();
            const grid = e.currentTarget;
            const currentlyDragged = this.state.draggedItem;
            if (!currentlyDragged || currentlyDragged.parentElement != grid) return;

            // Determine where the item SHOULD be placed.
            const afterElement = this._getDragAfterElement(grid, e.clientX, e.clientY);

            // Optimization: Prevent DOM updates if position hasn't changed to avoid jitter.
            if (currentlyDragged.nextSibling === afterElement) return;

            grid.insertBefore(currentlyDragged, afterElement);
        },

        // --- HELPER FUNCTIONS ---

        /**
         * Finds the sibling element that should come after the dragged item.
         * @private
         * @param {HTMLElement} container - The grid container element.
         * @param {number} x - The cursor's horizontal position.
         * @param {number} y - The cursor's vertical position.
         * @returns {HTMLElement|null} The sibling element to insert before.
         */
        _getDragAfterElement(container, x, y) {
            const LocalC = this.constants;
            const selector = `.${LocalC.DOM.WEARABLE_BUTTON_CLASS}:not(.${LocalC.DOM.WEARABLE_DRAGGING_CLASS})`;
            const draggableSiblings = [...container.querySelectorAll(selector)];

            for (const sibling of draggableSiblings) {
                const box = sibling.getBoundingClientRect();
                const isVerticallyBefore = y < box.top + box.height / 2;
                const isInRow = y >= box.top && y <= box.bottom;
                const isHorizontallyBefore = x < box.left + box.width / 2;

                if (isVerticallyBefore || (isInRow && isHorizontallyBefore)) {
                    return sibling;
                }
            }

            return null; // If after all other elements
        },
    };

    /**
     * @module TypingIndicatorMiniMod
     * @description Shows a "..." typing indicator in chat while the user is typing.
     */
    const TypingIndicatorMiniMod = {

        // --- MINI-MOD PROPERTIES ---

        /** @property {object|null} core - A reference to the core module. */
        core: null,

        /** @property {string} name - The display name of the minimod. */
        name: "Typing Indicator",

        /** @property {object} config - Holds user-configurable settings. */
        config: {
            /** @property {boolean} enabled - Master switch for this minimod. */
            enabled: true,
            /** @property {number} INDICATOR_INTERVAL - The time in milliseconds between each animation frame. */
            INDICATOR_INTERVAL: 1000,
            /** @property {number} RATE_LIMIT_MS - The cooldown period between sending chat messages. */
            RATE_LIMIT_MS: 550,
            /** @property {number} START_DELAY - A safe buffer before showing the indicator. */
            START_DELAY: 1000,
            /** @property {string[]} ANIMATION_FRAMES - The sequence of strings used for the typing animation. */
            ANIMATION_FRAMES: ['.', '..', '...'],
            /** @property {number} QUEUE_PROCESSOR_INTERVAL - How often to check the message queue for pending messages to send. */
            QUEUE_PROCESSOR_INTERVAL: 100,
        },

        /** @property {object} state - Dynamic state for this minimod. */
        state: {
            /** @property {HTMLElement|null} chatBoxElement - A reference to the game's chat input element. */
            chatBoxElement: null,
            /** @property {number|null} indicatorIntervalId - The ID of the interval used for the typing animation. */
            indicatorIntervalId: null,
            /** @property {number|null} startIndicatorTimeoutId - The ID of the timeout used to delay the start of the indicator. */
            startIndicatorTimeoutId: null,
            /** @property {number|null} queueProcessorIntervalId - The ID of the interval that processes the message queue. */
            queueProcessorIntervalId: null,
            /** @property {number} animationFrameIndex - The current index in the `ANIMATION_FRAMES` array. */
            animationFrameIndex: 0,
            /** @property {number} lastMessageSentTime - The timestamp of the last message sent to the server. */
            lastMessageSentTime: 0,
            /** @property {Array<{type: 'user'|'system', content: string}>} messageQueue - The queue of messages waiting to be sent. */
            messageQueue: [],
            /** @property {object} boundHandlers - Stores bound event handler functions for easy addition and removal of listeners. */
            boundHandlers: {},
        },

        /**
         * Defines the settings for this minimod.
         * @returns {Array<object>} An array of setting definition objects.
         */
        getSettings() {
            return [
                {
                    id: 'typing_indicator_enabled',
                    configKey: 'enabled',
                    label: 'Enable Typing Indicator',
                    desc: 'Shows "..." in chat while you are typing.',
                    type: 'checkbox'
                }
            ]
        },

        // --- MINI-MOD LIFECYCLE & HOOKS ---

        /**
         * Adds all necessary event listeners for this minimod.
         * @returns {void}
         */
        addEventListeners() {
            if (!this.config.enabled) return;

            this.state.chatBoxElement = document.getElementById(this.core.data.constants.DOM.CHAT_BOX);
            if (!this.state.chatBoxElement) return Logger.error("Could not find chatBox element. Mod will not function.");

            this.state.boundHandlers.handleFocus = this.handleFocus.bind(this);
            this.state.boundHandlers.handleBlur = this.handleBlur.bind(this);
            this.state.boundHandlers.handleKeyDown = this.handleKeyDown.bind(this);

            this.state.chatBoxElement.addEventListener('focus', this.state.boundHandlers.handleFocus);
            this.state.chatBoxElement.addEventListener('blur', this.state.boundHandlers.handleBlur);
            this.state.chatBoxElement.addEventListener('keydown', this.state.boundHandlers.handleKeyDown);

            // Start the queue processor, which will run continuously to send queued messages.
            this.startQueueProcessor();
            Logger.log("Typing indicator event listeners attached and queue processor started.");
        },

        /**
         * Cleans up all timers and event listeners.
         * @returns {void}
         */
        cleanup() {
            clearInterval(this.state.indicatorIntervalId);
            clearInterval(this.state.queueProcessorIntervalId);
            clearTimeout(this.state.startIndicatorTimeoutId);
            if (this.state.chatBoxElement) {
                this.state.chatBoxElement.removeEventListener('focus', this.state.boundHandlers.handleFocus);
                this.state.chatBoxElement.removeEventListener('blur', this.state.boundHandlers.handleBlur);
                this.state.chatBoxElement.removeEventListener('keydown', this.state.boundHandlers.handleKeyDown);
            }
        },

        // --- EVENT HANDLERS ---

        /**
         * Handles the `focus` event on the chat box.
         * @returns {void}
         */
        handleFocus() {
            if (!this.config.enabled) return;

            // Instead of starting immediately, set a timeout to begin the animation.
            // This prevents the indicator from flashing for accidental clicks or very fast messages.
            if (this.state.startIndicatorTimeoutId) clearTimeout(this.state.startIndicatorTimeoutId);
            this.state.startIndicatorTimeoutId = setTimeout(() => {
                this.startTypingIndicator();
            }, this.config.START_DELAY);
        },

        /**
         * Handles the `blur` event on the chat box.
         * @returns {void}
         */
        handleBlur() {
            if (!this.config.enabled) return;

            clearTimeout(this.state.startIndicatorTimeoutId);
            this.stopTypingIndicator();
        },

        /**
         * Intercepts the 'Enter' key press to queue the message.
         * @param {KeyboardEvent} event - The DOM keyboard event.
         * @returns {void}
         */
        handleKeyDown(event) {
            if (this.config.enabled && event.key === 'Enter') {
                // Prevent the game from sending the message. We will handle it.
                event.preventDefault();
                clearTimeout(this.state.startIndicatorTimeoutId);

                const message = this.state.chatBoxElement.value.trim();
                if (message) {
                    this.queueUserMessage(message);
                }

                // Clear the chat box and stop the indicator, as the user is done typing.
                this.state.chatBoxElement.value = '';
                this.stopTypingIndicator();
            }
        },

        // --- CORE LOGIC ---

        /**
         * Starts the animation loop.
         * @returns {void}
         */
        startTypingIndicator() {
            if (!this.config.enabled || this.state.indicatorIntervalId) return; // Already running

            Logger.log("Starting typing indicator.");
            this.state.animationFrameIndex = 0;

            // Run once immediately, then start the interval
            this.animateIndicator();
            this.state.indicatorIntervalId = setInterval(this.animateIndicator.bind(this), this.config.INDICATOR_INTERVAL);
        },

        /**
         * Stops the animation loop and cleans up.
         * @returns {void}
         */
        stopTypingIndicator() {
            if (!this.state.indicatorIntervalId) return; // Already stopped

            Logger.log("Stopping typing indicator and cleaning up queue.");
            clearInterval(this.state.indicatorIntervalId);
            this.state.indicatorIntervalId = null;

            // Remove any pending system messages
            this.state.messageQueue = this.state.messageQueue.filter(msg => msg.type !== 'system');

            // Queue one final, empty message to clear the indicator that might be visible in chat.
            this.queueSystemMessage('');
        },

        /**
         * Queues the next frame of the animation to be sent.
         * @returns {void}
         */
        animateIndicator() {
            const frame = this.config.ANIMATION_FRAMES[this.state.animationFrameIndex];
            this.queueSystemMessage(frame);

            // Cycle to the next frame
            this.state.animationFrameIndex = (this.state.animationFrameIndex + 1) % this.config.ANIMATION_FRAMES.length;
        },

        // --- RATE LIMIT & QUEUE MANAGEMENT ---

        /**
         * Starts the interval that processes the message queue.
         * @returns {void}
         */
        startQueueProcessor() {
            if (this.state.queueProcessorIntervalId) return;
            this.state.queueProcessorIntervalId = setInterval(this.processMessageQueue.bind(this), this.config.QUEUE_PROCESSOR_INTERVAL);
        },

        /**
         * Adds a user-typed message to the front of the queue.
         * @param {string} message - The user's chat message.
         * @returns {void}
         */
        queueUserMessage(message) {
            Logger.log(`Queueing user message: "${message}"`);
            this.state.messageQueue.unshift({ type: 'user', content: message });
        },

        /**
         * Adds a system message (like the indicator) to the back of the queue.
         * @param {string} message - The system message content.
         * @returns {void}
         */
        queueSystemMessage(message) {
            // Optimization: Don't queue up a ton of indicator dots. 
            // If the last message in the queue is also an indicator, replace it.
            const lastInQueue = this.state.messageQueue[this.state.messageQueue.length - 1];
            if (lastInQueue && lastInQueue.type === 'system') {
                this.state.messageQueue[this.state.messageQueue.length - 1].content = message;
            } else {
                this.state.messageQueue.push({ type: 'system', content: message });
            }
        },

        /**
         * Checks the queue and sends the next message if the rate limit has passed.
         * @returns {void}
         */
        processMessageQueue() {
            const CoreC = this.core.data.constants;
            const canSendMessage = (Date.now() - this.state.lastMessageSentTime) > this.config.RATE_LIMIT_MS;

            if (canSendMessage && this.state.messageQueue.length > 0) {
                const messageToSend = this.state.messageQueue.shift(); // Get the next message

                this.core.sendGamePacket(CoreC.PACKET_TYPES.CHAT, [messageToSend.content]);
                this.state.lastMessageSentTime = Date.now();

                if (messageToSend.type === 'user') {
                    Logger.log(`Sent queued user message: "${messageToSend.content}"`);
                }
            }
        }
    };

    /**
     * @module ProximityChatMiniMod
     * @description Displays nearby player chats in a Minecraft/Roblox-style chatbox,
     * showing player names, leaderboard ranks, and timestamps.
     */
    const ProximityChatMiniMod = {
        // --- MINI-MOD PROPERTIES ---

        /** @property {object|null} core - A reference to the core module. */
        core: null,

        /** @property {string} name - The display name of the minimod. */
        name: "Proximity Chat",

        /** @property {object} config - Holds user-configurable settings. */
        config: {
            /** @property {boolean} enabled - Master switch for this minimod. */
            enabled: true,
            /** @property {string} TOGGLE_KEY - The hotkey to show or hide the toolbar. */
            TOGGLE_KEY: 'T',
            /** @property {number} maxMessages - The maximum number of messages to keep in the chatbox. */
            maxMessages: 12
        },

        /** @property {object} constants - Constants specific to this minimod. */
        constants: {
            DOM: {
                CHATBOX_CONTAINER_ID: 'proximityChatboxContainer',
                CHATBOX_MESSAGES_ID: 'proximityChatboxMessages',
                CHAT_MESSAGE_CLASS: 'proximityChatMessage'
            }
        },

        /** @property {object} state - Dynamic state for this minimod. */
        state: {
            /** @property {boolean} isVisible - Whether the toolbar UI is currently shown. */
            isVisible: true,
            /** @property {Map<number, object>} players - Maps player SID to their data {id, name}. */
            players: new Map(),
            /** @property {Map<number, number>} leaderboard - Maps player SID to their rank. */
            leaderboard: new Map(),
            /** @property {HTMLElement|null} chatboxContainer - Reference to the main chatbox UI element. */
            chatboxContainer: null,
            /** @property {HTMLElement|null} messagesContainer - Reference to the inner element that holds messages. */
            messagesContainer: null,
            /** @property {object} boundHandlers - Stores bound event handler functions for easy addition and removal of listeners. */
            boundHandlers: {}
        },

        /**
         * Defines the settings for this minimod.
         * @returns {Array<object>} An array of setting definition objects.
         */
        getSettings() {
            return [
                {
                    id: 'proximity_chat_enabled',
                    configKey: 'enabled',
                    label: 'Enable Proximity Chat',
                    desc: 'Shows nearby chats in a custom chatbox.',
                    type: 'checkbox',
                    onChange: (value) => this.toggleFeature(value)
                },
                {
                    id: 'proximity_chat_toggle_key',
                    configKey: 'TOGGLE_KEY',
                    label: 'Toggle Chatbox Key',
                    desc: 'Press to show or hide the chatbox.',
                    type: 'keybind'
                },
                {
                    id: 'proximity_chat_max_messages',
                    configKey: 'maxMessages',
                    label: 'Max Chat Messages',
                    desc: 'The number of messages to show before old ones disappear.',
                    type: 'number', min: 10, max: 500
                }
            ];
        },

        // --- MINI-MOD LIFECYCLE & HOOKS ---

        /**
         * Initializes the minimod by creating the chatbox UI.
         * @returns {void}
         */
        init() {
            if (!this.config.enabled) return;

            const CoreC = this.core.data.constants;

            // Wait for the main game UI to be ready before injecting our chatbox
            this.core.waitForElementsToLoad(CoreC.DOM.GAME_UI).then(gameUI => {
                const chatboxContainer = document.createElement('div');
                chatboxContainer.id = this.constants.DOM.CHATBOX_CONTAINER_ID;

                this.core.registerFocusableElement(chatboxContainer.id);

                const messagesContainer = document.createElement('div');
                messagesContainer.id = this.constants.DOM.CHATBOX_MESSAGES_ID;

                chatboxContainer.appendChild(messagesContainer);
                gameUI.appendChild(chatboxContainer);

                this.state.chatboxContainer = chatboxContainer;
                this.state.messagesContainer = messagesContainer;

                this.state.boundHandlers.handleKeyDown = (e) => {
                    if (!this.config.enabled || this.core.isInputFocused()) return;

                    if (e.key.toUpperCase() === this.config.TOGGLE_KEY) {
                        this.state.isVisible = !this.state.isVisible;
                        this.state.chatboxContainer.style.display = this.state.isVisible ? CoreC.CSS.DISPLAY_FLEX : CoreC.CSS.DISPLAY_NONE;
                    }
                };

                document.addEventListener('keydown', this.state.boundHandlers.handleKeyDown);
            });
        },
        
        /**
         * Returns the CSS rules required for styling the chatbox.
         * @returns {string} The complete CSS string.
         */
        applyCSS() {
            const LocalC = this.constants;
            return `
                #${LocalC.DOM.CHATBOX_CONTAINER_ID} {
                    position: absolute;
                    bottom: 215px; /* Positioned above the action bar */
                    left: 20px;
                    width: 400px;
                    max-width: 50%;
                    height: 250px;
                    opacity: 0.75;
                    background-color: rgba(0, 0, 0, 0.33333);
                    border-radius: 4px;
                    color: white;
                    font-family: 'Hammersmith One', sans-serif;
                    font-size: 16px;
                    display: flex;
                    flex-direction: column-reverse; /* New messages appear at the bottom */
                    pointer-events: all;
                    z-index: 10; /* Ensure it's above most game elements but below menus */
                    
                    /* --- KEY CHANGE: Scrolling is now handled by the main container --- */
                    overflow-y: auto; 
                    scrollbar-width: thin;
                    scrollbar-color: rgba(255, 255, 255, 0.3) rgba(0, 0, 0, 0.2);
                    transition: opacity 1s;

                    &:hover {
                        opacity: 1;
                    }

                    &:not(:has(.${LocalC.DOM.CHAT_MESSAGE_CLASS})):is(&, &:hover) {
                        opacity: 0;
                    }
                }

                #${LocalC.DOM.CHATBOX_CONTAINER_ID}::-webkit-scrollbar {
                    width: 6px;
                }

                #${LocalC.DOM.CHATBOX_CONTAINER_ID}::-webkit-scrollbar-track {
                    background: rgba(0, 0, 0, 0.2);
                }

                #${LocalC.DOM.CHATBOX_CONTAINER_ID}::-webkit-scrollbar-thumb {
                    background-color: rgba(255, 255, 255, 0.3);
                    border-radius: 3px;
                }

                #${LocalC.DOM.CHATBOX_MESSAGES_ID} {
                    /* --- KEY CHANGE: Removed flex properties and overflow from the inner container --- */
                    padding: 8px;
                    word-wrap: break-word;
                }

                .${LocalC.DOM.CHAT_MESSAGE_CLASS} {
                    margin-top: 4px;
                    text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7);
                    animation: fadeIn 0.3s ease-in-out;
                    transition: 1s;
                }
                
                @keyframes fadeIn {
                    from { opacity: 0; transform: translateY(10px); }
                    to { opacity: 1; transform: translateY(0); }
                }

                .${LocalC.DOM.CHAT_MESSAGE_CLASS} .timestamp {
                    color: #AAAAAA;
                }

                .${LocalC.DOM.CHAT_MESSAGE_CLASS} .player-name {
                    font-weight: bold;
                    color: #FFFFFF;
                }

                .${LocalC.DOM.CHAT_MESSAGE_CLASS} .message-content {
                    color: #FFFFFF;
                }
            `;
        },
        
        /**
         * Cleans up all UI created by this minimod.
         * @returns {void}
         */
        cleanup() {
            this.state.chatboxContainer?.remove();
            this.state.chatboxContainer = null;
            this.state.messagesContainer = null;
            this.state.players.clear();
            this.state.leaderboard.clear();
            
            document.removeEventListener('keydown', this.state.boundHandlers.handleKeyDown);
        },

        /**
         * Toggles the feature's visibility.
         * @param {boolean} isEnabled - The new enabled state.
         */
        toggleFeature(isEnabled) {
            if (this.state.chatboxContainer) {
                this.state.chatboxContainer.style.display = isEnabled ? 'flex' : 'none';
            }
        },

        /**
         * Handles incoming game packets to update the minimod's state.
         * @param {string} packetName - The human-readable name of the packet.
         * @param {object} packetData - The parsed data object from the packet.
         */
        onPacket(packetName, packetData) {
            if (!this.config.enabled) return;

            switch (packetName) {
                case 'Add Player':
                    if (packetData.sid) {
                        this.state.players.set(packetData.sid, { id: packetData.id, name: packetData.name });
                    }
                    break;

                case 'Remove Player': {
                    const idToRemove = packetData.id;
                    for (const [sid, playerData] of this.state.players.entries()) {
                        if (playerData.id === idToRemove) {
                            this.state.players.delete(sid);
                            break;
                        }
                    }
                    break;
                }

                case 'Leaderboard Update':
                    this.state.leaderboard.clear();
                    packetData.leaderboard.forEach((player, index) => {
                        this.state.leaderboard.set(player.sid, index + 1);
                    });
                    break;

                case 'Receive Chat':
                    if (!(/^\.+$/.test(packetData.message))) this.addChatMessage(packetData.sid, packetData.message);
                    break;

                case 'Client Player Death':
                case 'Setup Game': // Clear on respawn or new game
                    this.state.players.clear();
                    this.state.leaderboard.clear();
                    if (this.state.messagesContainer) {
                        this.state.messagesContainer.innerHTML = '';
                    }
                    break;
            }
        },
        
        /**
         * Creates and adds a new chat message to the UI.
         * @param {number} sid - The sender's session ID.
         * @param {string} message - The chat message content.
         */
        async addChatMessage(sid, message) {
            if (!this.config.enabled || !this.state.messagesContainer) return;

            const playerInfo = this.state.players.get(sid);
            const rank = this.state.leaderboard.get(sid);

            let playerName = playerInfo ? playerInfo.name : `Player ${sid}`;
            if (rank) {
                playerName = `[${rank}] ${playerName}`;
            }

            const timestamp = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });

            // Create elements safely to prevent HTML injection
            const msgElement = document.createElement('div');
            msgElement.className = this.constants.DOM.CHAT_MESSAGE_CLASS;

            const timeSpan = document.createElement('span');
            timeSpan.className = 'timestamp';
            timeSpan.textContent = `[${timestamp}] `;
            
            const nameSpan = document.createElement('span');
            nameSpan.className = 'player-name';
            nameSpan.textContent = `${playerName}`;
            // Style ranked players differently for emphasis
            if (rank) {
                nameSpan.style.color = '#FFD700'; // Gold color
            }

            const contentSpan = document.createElement('span');
            contentSpan.className = 'message-content';
            contentSpan.textContent = `: ${message}`;

            msgElement.append(timeSpan, nameSpan, contentSpan);
            
            // Add to UI and manage message limit
            this.state.messagesContainer.appendChild(msgElement);

            if (this.state.messagesContainer.children.length > this.config.maxMessages) {
                const lastMessage = this.state.messagesContainer.firstChild;

                lastMessage.style.opacity = '0';
                lastMessage.style.translateY = '-10px';

                await this.core.wait(1000);

                this.state.messagesContainer.removeChild(lastMessage);
            }
        },
    };

    // --- REGISTER MINI-MODS & INITIALIZE ---

    MooMooUtilityMod.registerMod(SettingsManagerMiniMod);
    MooMooUtilityMod.registerMod(ScrollInventoryMiniMod);
    MooMooUtilityMod.registerMod(WearablesToolbarMiniMod);
    MooMooUtilityMod.registerMod(TypingIndicatorMiniMod);
    MooMooUtilityMod.registerMod(ProximityChatMiniMod);

    MooMooUtilityMod.init();
})();