FlatMMO Quick Actions

Adds configurable worship teleport, /stuck, /dig, key bindings, and /tb buttons under your inventory (FlatMMOPlus plugin)

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

Advertisement:

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         FlatMMO Quick Actions
// @namespace    com.flatmmo.quick-actions
// @version      4.0
// @description  Adds configurable worship teleport, /stuck, /dig, key bindings, and /tb buttons under your inventory (FlatMMOPlus plugin)
// @author       You
// @license      MIT
// @match        *://flatmmo.com/play.php*
// @grant        none
// @require      https://update.greasyfork.org/scripts/544062/FlatMMOPlus.js
// ==/UserScript==

(function () {
    'use strict';

    // ================= CONFIG =================
    // If buttons don't work, right-click the relevant element in-game,
    // choose Inspect, and update the selector below with its id/class.
    const INVENTORY_SELECTOR = '#ui-panel-inventory-content';
    const STORAGE_KEY = 'fmmoQuickActionsConfig';
    // ============================================

    // Every button the control panel can show/hide/reorder.
    // type "worship" = click the real worship tile; type "tb" = send the teleport-book
    // command straight over the websocket; type "panel" = switch to a game panel via
    // FlatMMOPlus.setPanel(); type "keybinds" = open the key bindings modal; type "stuck"
    // = send the /stuck chat command.
    const MASTER_ITEMS = {
        ev: { type: 'worship', label: 'Everbrook', img: 'teleport_everbrook', requiredLevel: 5, requiredPoints: 3 },
        mv: { type: 'worship', label: 'Mystic Vale', img: 'teleport_mysticvale', requiredLevel: 10, requiredPoints: 3 },
        om: { type: 'worship', label: 'Omboko', img: 'teleport_omboko', requiredLevel: 20, requiredPoints: 3 },
        dh: { type: 'worship', label: 'Dock Haven', img: 'teleport_dock_haven', requiredLevel: 25, requiredPoints: 3 },
        jo: { type: 'worship', label: 'Jafa Outpost', img: 'teleport_jafa_outpost', requiredLevel: 40, requiredPoints: 3 },
        fv: { type: 'worship', label: 'Frostvale', img: 'teleport_frostvale', requiredLevel: 45, requiredPoints: 3 },
        mp: { type: 'worship', label: 'Mass Pickup', img: 'mass_pickup', requiredLevel: 55, requiredPoints: 1 },
        dig: { type: 'worship', label: 'Dig', img: 'dig', requiredLevel: 9, requiredPoints: 0 },
        rs: { type: 'worship', label: 'Remote Sell', img: 'remote_sell', requiredLevel: 7, requiredPoints: 5 },
        tm: { type: 'worship', label: 'Timers', img: 'timers', requiredLevel: 15, requiredPoints: 0 },
        fc: { type: 'worship', label: 'Focus', img: 'focus', requiredLevel: 30, requiredPoints: 5 },
        hb: { type: 'worship', label: 'Hell Bury', img: 'auto_hell_burying', requiredLevel: 35, requiredPoints: 0 },
        cl: { type: 'worship', label: 'Clarity', img: 'clarity', requiredLevel: 60, requiredPoints: 15 },
        ch: { type: 'tb', label: 'Chef\u2019s House', scroll: 'chefs_house_teleport_scroll', displayImg: 'images/items/chefs_hat.png' },
        th: { type: 'tb', label: 'Thieves Hideout', scroll: 'thieves_hideout_teleport_scroll', displayImg: 'images/icons/stealing.png' },
        gh: { type: 'tb', label: 'Greenhouse', scroll: 'greenhouse_teleport_scroll', displayImg: 'images/icons/farming.png' },
        rg: { type: 'tb', label: 'Rogue\u2019s Grave', scroll: 'rogue_teleport_scroll', displayImg: 'images/items/sand.png' },
        pm: { type: 'tb', label: 'Phantos Mansion', scroll: 'phantos_teleport_scroll', displayImg: 'images/ui/cemetery_child.png' },
        stuck: { type: 'stuck', label: 'stuck' },
        hunting: { type: 'panel', label: 'Hunting', panelId: 'hunting', imgSrc: 'images/icons/hunting_large.png' },
        keybinds: { type: 'keybinds', label: 'Key Bindings', imgSrc: 'images/ui/settings.png' },
    };

    // Default layout: matches what you already had set up.
    const DEFAULT_CONFIG = {
        perRow: 5,
        order: ['ev', 'rs', 'dig', 'mv', 'tm', 'om', 'dh', 'fc', 'hb', 'jo', 'fv', 'mp', 'cl', 'stuck', 'hunting', 'keybinds', 'ch', 'th', 'gh', 'rg', 'pm'],
        enabled: {
            ev: false, mv: false, om: false, dh: false, jo: false, fv: false,
            mp: false, dig: false, ch: false, rg: false, gh: false, pm: false, stuck: false, hunting: false, keybinds: false,
            rs: false, tm: false, fc: false, hb: false, cl: false, th: false,
        },
        hotkeys: {},
    };

    function structuredCloneConfig(cfg) {
        return JSON.parse(JSON.stringify(cfg));
    }

    function loadConfig() {
        try {
            const raw = localStorage.getItem(STORAGE_KEY);
            if (!raw) return structuredCloneConfig(DEFAULT_CONFIG);
            const parsed = JSON.parse(raw);
            // Make sure any newly-added master items not yet in a saved config still show up (disabled) rather than crashing.
            const merged = structuredCloneConfig(DEFAULT_CONFIG);
            merged.perRow = parsed.perRow || merged.perRow;
            if (Array.isArray(parsed.order)) {
                merged.order = parsed.order.filter((id) => MASTER_ITEMS[id]);
                Object.keys(MASTER_ITEMS).forEach((id) => {
                    if (!merged.order.includes(id)) merged.order.push(id);
                });
            }
            if (parsed.enabled) {
                Object.keys(merged.enabled).forEach((id) => {
                    if (typeof parsed.enabled[id] === 'boolean') merged.enabled[id] = parsed.enabled[id];
                });
            }
            if (parsed.hotkeys && typeof parsed.hotkeys === 'object') {
                Object.keys(parsed.hotkeys).forEach((id) => {
                    if (MASTER_ITEMS[id] && typeof parsed.hotkeys[id] === 'string') {
                        merged.hotkeys[id] = parsed.hotkeys[id];
                    }
                });
            }
            return merged;
        } catch (e) {
            console.warn('FlatMMO+ Quick Actions: could not load saved config, using defaults.', e);
            return structuredCloneConfig(DEFAULT_CONFIG);
        }
    }

    function saveConfig(cfg) {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(cfg));
    }

    let currentConfig = loadConfig();

    // ---- Worship level/points, read live from the game's own UI ----

    function getWorshipLevel() {
        const el = document.getElementById('ui-skill-worship-level');
        return el ? parseInt(el.textContent, 10) || 0 : 0;
    }

    function getWorshipPoints() {
        const el = document.querySelector('display-backend-value[data-id="warship-points"]');
        return el ? parseInt(el.textContent, 10) || 0 : 0;
    }

    function applyWorshipButtonState(btn, entry) {
        const points = getWorshipPoints();
        const meetsPoints = points >= (entry.requiredPoints || 0);
        if (meetsPoints) {
            btn.style.opacity = '1';
            btn.style.filter = 'none';
            btn.disabled = false;
            btn.title = entry.label;
        } else {
            btn.style.opacity = '0.4';
            btn.style.filter = 'grayscale(70%)';
            btn.disabled = true;
            btn.title = `${entry.label} \u2014 needs ${entry.requiredPoints} worship points (you have ${points})`;
        }
    }

    function refreshWorshipButtonStates() {
        document.querySelectorAll('#fmmo-quick-actions [data-worship-id]').forEach((btn) => {
            const entry = MASTER_ITEMS[btn.dataset.worshipId];
            if (entry) applyWorshipButtonState(btn, entry);
        });
    }

    let worshipPointsObserver = null;

    function watchWorshipPoints() {
        const el = document.querySelector('display-backend-value[data-id="warship-points"]');
        if (!el) {
            // Not rendered yet (e.g. not logged in) - try again shortly.
            setTimeout(watchWorshipPoints, 500);
            return;
        }
        if (worshipPointsObserver) return; // already watching

        worshipPointsObserver = new MutationObserver(refreshWorshipButtonStates);
        worshipPointsObserver.observe(el, { childList: true, characterData: true, subtree: true });
    }

    // ---- Action handlers ----
    // These use the real FlatMMOPlus.sendMessage() / setPanel() APIs instead of
    // simulating clicks/keystrokes wherever the wire protocol is known.

    function clickWorshipTile(imgFragment) {
        const findTile = () => document.querySelector(`#ui-panel-worship-content img[src*="${imgFragment}"]`);
        let img = findTile();
        if (img) {
            (img.closest('.worship-entry') || img).click();
            return;
        }
        // Tile isn't in the DOM yet (worship tab hasn't been opened this session).
        // Switch to it via the real FlatMMOPlus API, click the tile, then switch back.
        const previousPanel = window.FlatMMOPlus.currentPanel;
        window.FlatMMOPlus.setPanel('worship');
        setTimeout(() => {
            img = findTile();
            if (img) {
                (img.closest('.worship-entry') || img).click();
            } else {
                alert('FlatMMO+ Quick Actions: could not find that worship teleport tile even after opening the Worship tab.');
            }
            if (previousPanel) window.FlatMMOPlus.setPanel(previousPanel);
        }, 150);
    }

    function teleBookTeleport(scroll) {
        window.FlatMMOPlus.sendMessage(`TELE_BOOK=${scroll}`);
    }

    function openKeyBindings() {
        // These are called directly since they were plain top-level functions.
        if (typeof window.close_modal === 'function') {
            try {
                window.close_modal('settings-modal');
            } catch (e) {
                // ignore - settings modal may not have been open anyway
            }
        }
        if (typeof window.open_key_bindings_modal === 'function') {
            window.open_key_bindings_modal();
        } else {
            alert(
                'FlatMMO+ Quick Actions: could not find open_key_bindings_modal(). The game may have changed its scripts.'
            );
        }
    }

    function describeKeyEvent(e) {
        const parts = [];
        if (e.ctrlKey) parts.push('Ctrl');
        if (e.altKey) parts.push('Alt');
        if (e.shiftKey) parts.push('Shift');
        parts.push(e.code);
        return parts.join('+');
    }

    function prettifyCombo(combo) {
        if (!combo) return '';
        return combo
            .split('+')
            .map((part) => {
                if (part.startsWith('Key')) return part.slice(3);
                if (part.startsWith('Digit')) return part.slice(5);
                return part;
            })
            .join('+');
    }

    function isTypingTarget(target) {
        if (!target) return false;
        const tag = target.tagName ? target.tagName.toLowerCase() : '';
        return tag === 'input' || tag === 'textarea' || target.isContentEditable;
    }

    let hotkeyRecordingActive = false;

    // Global hotkeys work even if the matching button isn't currently shown in the panel.
    document.addEventListener(
        'keydown',
        (e) => {
            if (hotkeyRecordingActive) return; // a hotkey is being recorded in the settings menu right now
            if (document.getElementById('fmmo-quick-actions-settings')) return; // don't fire while configuring
            if (isTypingTarget(e.target)) return; // don't fire while typing in chat, etc.
            if (!currentConfig.hotkeys) return;

            const combo = describeKeyEvent(e);
            const match = Object.keys(currentConfig.hotkeys).find(
                (id) => currentConfig.hotkeys[id] === combo
            );
            if (match) {
                e.preventDefault();
                e.stopPropagation();
                executeItem(match);
            }
        },
        true
    );

    function executeItem(id) {
        const entry = MASTER_ITEMS[id];
        if (!entry) return;
        if (entry.type === 'worship') {
            const points = getWorshipPoints();
            if (points < (entry.requiredPoints || 0)) {
                alert(
                    `${entry.label} needs ${entry.requiredPoints} worship points, you currently have ${points}.`
                );
                return;
            }
            return clickWorshipTile(entry.img);
        }
        if (entry.type === 'tb') return teleBookTeleport(entry.scroll);
        if (entry.type === 'panel') return window.FlatMMOPlus.setPanel(entry.panelId);
        if (entry.type === 'keybinds') return openKeyBindings();
        if (entry.type === 'stuck') return window.FlatMMOPlus.sendMessage('CHAT=/stuck');
    }

    // ---- UI building ----

    function getIconSrc(entry) {
        if (entry.type === 'worship') return `images/worship/${entry.img}.png`;
        if (entry.type === 'tb') return entry.displayImg || `images/items/${entry.scroll}.png`;
        if (entry.type === 'panel' || entry.type === 'keybinds') return entry.imgSrc;
        return null; // 'stuck' has no icon
    }

    function makeButton(label, onClick) {
        const btn = document.createElement('button');
        btn.textContent = label;
        btn.style.cssText = `
            padding: 4px 8px;
            font-size: 12px;
            cursor: pointer;
            border-radius: 4px;
            border: 1px solid rgb(205, 205, 205);
            background-color: rgb(225, 225, 225);
            color: #222;
        `;
        btn.addEventListener('click', onClick);
        return btn;
    }

    function makeImageButton(label, imgSrc, onClick) {
        const btn = document.createElement('button');
        btn.title = label;
        btn.style.cssText = `
            padding: 4px;
            width: 56px;
            height: 56px;
            cursor: pointer;
            border-radius: 4px;
            border: 1px solid rgb(205, 205, 205);
            background-color: rgb(225, 225, 225);
            display: flex;
            align-items: center;
            justify-content: center;
        `;
        const img = document.createElement('img');
        img.src = imgSrc;
        img.alt = label;
        img.style.cssText = 'width: 100%; height: 100%; object-fit: contain;';
        btn.appendChild(img);
        btn.addEventListener('click', onClick);
        return btn;
    }

    function buildButtonFor(id) {
        const entry = MASTER_ITEMS[id];
        const iconSrc = getIconSrc(entry);
        if (!iconSrc) {
            // 'stuck'
            return makeButton(entry.label, () => executeItem(id));
        }
        const btn = makeImageButton(entry.label, iconSrc, () => executeItem(id));
        if (entry.type === 'worship') {
            btn.dataset.worshipId = id;
            applyWorshipButtonState(btn, entry);
        }
        return btn;
    }

    function chunk(arr, size) {
        const rows = [];
        for (let i = 0; i < arr.length; i += size) {
            rows.push(arr.slice(i, i + size));
        }
        return rows;
    }

    function buildPanel() {
        const panel = document.createElement('div');
        panel.id = 'fmmo-quick-actions';
        panel.style.cssText = `
            margin-top: 8px;
            padding: 8px;
            display: flex;
            flex-direction: column;
            gap: 6px;
            background: transparent;
            border-radius: 6px;
        `;

        const enabledIds = currentConfig.order.filter((id) => currentConfig.enabled[id]);
        const rows = chunk(enabledIds, currentConfig.perRow);

        rows.forEach((row) => {
            const rowDiv = document.createElement('div');
            rowDiv.style.cssText = 'display: flex; gap: 6px;';
            row.forEach((id) => {
                rowDiv.appendChild(buildButtonFor(id));
            });
            panel.appendChild(rowDiv);
        });

        return panel;
    }

    function refreshPanel() {
        const old = document.getElementById('fmmo-quick-actions');
        if (!old) return;
        const fresh = buildPanel();
        old.replaceWith(fresh);
    }

    function openSettingsPanel() {
        if (document.getElementById('fmmo-quick-actions-settings')) return; // already open

        const overlay = document.createElement('div');
        overlay.id = 'fmmo-quick-actions-settings';
        overlay.style.cssText = `
            position: fixed;
            top: 0; left: 0; right: 0; bottom: 0;
            background: rgba(0,0,0,0.5);
            z-index: 100000;
            display: flex;
            align-items: center;
            justify-content: center;
        `;

        const box = document.createElement('div');
        box.style.cssText = `
            background: rgb(240, 230, 210);
            border: 2px solid rgb(180, 160, 130);
            border-radius: 8px;
            padding: 16px;
            width: 460px;
            max-height: 80vh;
            overflow-y: auto;
            font-family: sans-serif;
            color: #222;
        `;

        const title = document.createElement('div');
        title.textContent = 'Quick Actions Settings';
        title.style.cssText = 'font-weight: bold; font-size: 14px; margin-bottom: 8px;';
        box.appendChild(title);

        const perRowRow = document.createElement('div');
        perRowRow.style.cssText = 'display: flex; align-items: center; gap: 6px; margin-bottom: 10px; font-size: 13px;';
        const perRowLabel = document.createElement('label');
        perRowLabel.textContent = 'Buttons per row:';
        const perRowInput = document.createElement('input');
        perRowInput.type = 'number';
        perRowInput.min = '1';
        perRowInput.max = '10';
        perRowInput.value = currentConfig.perRow;
        perRowInput.style.cssText = 'width: 50px;';
        perRowRow.appendChild(perRowLabel);
        perRowRow.appendChild(perRowInput);
        box.appendChild(perRowRow);

        // Working copy of order so Cancel doesn't mutate saved config
        let workingOrder = currentConfig.order.slice();
        const workingEnabled = Object.assign({}, currentConfig.enabled);
        const workingHotkeys = Object.assign({}, currentConfig.hotkeys);

        const list = document.createElement('div');
        box.appendChild(list);

        function renderList() {
            list.innerHTML = '';
            workingOrder.forEach((id, index) => {
                const item = MASTER_ITEMS[id];
                const row = document.createElement('div');
                row.style.cssText = `
                    display: flex;
                    align-items: center;
                    gap: 6px;
                    padding: 4px 0;
                    border-bottom: 1px solid rgb(210, 195, 170);
                    font-size: 13px;
                `;

                const locked = item.type === 'worship' && getWorshipLevel() < (item.requiredLevel || 0);

                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                if (locked) {
                    workingEnabled[id] = false; // can't be enabled until the level requirement is met
                }
                checkbox.checked = !!workingEnabled[id];
                checkbox.disabled = locked;
                checkbox.addEventListener('change', () => {
                    workingEnabled[id] = checkbox.checked;
                });

                const label = document.createElement('span');
                label.textContent = locked
                    ? `${item.label} (requires level ${item.requiredLevel})`
                    : item.label;
                label.style.cssText = locked ? 'flex: 1; opacity: 0.5;' : 'flex: 1;';

                const iconSrc = getIconSrc(item);
                let iconEl;
                if (iconSrc) {
                    iconEl = document.createElement('img');
                    iconEl.src = iconSrc;
                    iconEl.alt = '';
                    iconEl.style.cssText = 'width: 20px; height: 20px; object-fit: contain;';
                } else {
                    iconEl = document.createElement('span');
                    iconEl.style.cssText = 'display: inline-block; width: 20px;';
                }

                const upBtn = document.createElement('button');
                upBtn.textContent = '\u2191';
                upBtn.disabled = index === 0;
                upBtn.style.cssText = 'cursor: pointer;';
                upBtn.addEventListener('click', () => {
                    [workingOrder[index - 1], workingOrder[index]] = [workingOrder[index], workingOrder[index - 1]];
                    renderList();
                });

                const downBtn = document.createElement('button');
                downBtn.textContent = '\u2193';
                downBtn.disabled = index === workingOrder.length - 1;
                downBtn.style.cssText = 'cursor: pointer;';
                downBtn.addEventListener('click', () => {
                    [workingOrder[index + 1], workingOrder[index]] = [workingOrder[index], workingOrder[index + 1]];
                    renderList();
                });

                const hotkeyBtn = document.createElement('button');
                hotkeyBtn.style.cssText = 'cursor: pointer; font-size: 11px; min-width: 78px;';
                function updateHotkeyLabel() {
                    const combo = workingHotkeys[id];
                    hotkeyBtn.textContent = combo ? prettifyCombo(combo) : 'Set key';
                }
                updateHotkeyLabel();
                hotkeyBtn.addEventListener('click', () => {
                    hotkeyBtn.textContent = 'Press a key\u2026';
                    hotkeyRecordingActive = true;
                    const captureListener = (ev) => {
                        ev.preventDefault();
                        ev.stopPropagation();
                        // Ignore a standalone modifier press; wait for the real key.
                        if (['Control', 'Alt', 'Shift', 'Meta'].includes(ev.key)) return;
                        if (ev.key === 'Escape') {
                            document.removeEventListener('keydown', captureListener, true);
                            hotkeyRecordingActive = false;
                            updateHotkeyLabel();
                            return;
                        }
                        workingHotkeys[id] = describeKeyEvent(ev);
                        document.removeEventListener('keydown', captureListener, true);
                        hotkeyRecordingActive = false;
                        updateHotkeyLabel();
                    };
                    document.addEventListener('keydown', captureListener, true);
                });

                const clearHotkeyBtn = document.createElement('button');
                clearHotkeyBtn.textContent = '\u2715';
                clearHotkeyBtn.title = 'Clear hotkey';
                clearHotkeyBtn.style.cssText = 'cursor: pointer; font-size: 11px;';
                clearHotkeyBtn.addEventListener('click', () => {
                    delete workingHotkeys[id];
                    updateHotkeyLabel();
                });

                row.appendChild(checkbox);
                row.appendChild(iconEl);
                row.appendChild(label);
                row.appendChild(upBtn);
                row.appendChild(downBtn);
                row.appendChild(hotkeyBtn);
                row.appendChild(clearHotkeyBtn);
                list.appendChild(row);
            });
        }
        renderList();

        const buttonRow = document.createElement('div');
        buttonRow.style.cssText = 'display: flex; gap: 8px; margin-top: 12px; justify-content: flex-end;';

        const resetBtn = document.createElement('button');
        resetBtn.textContent = 'Reset to defaults';
        resetBtn.style.cssText = 'margin-right: auto; cursor: pointer;';
        resetBtn.addEventListener('click', () => {
            const def = structuredCloneConfig(DEFAULT_CONFIG);
            workingOrder = def.order.slice();
            Object.keys(workingEnabled).forEach((k) => delete workingEnabled[k]);
            Object.assign(workingEnabled, def.enabled);
            Object.keys(workingHotkeys).forEach((k) => delete workingHotkeys[k]);
            Object.assign(workingHotkeys, def.hotkeys);
            perRowInput.value = def.perRow;
            renderList();
        });

        const cancelBtn = document.createElement('button');
        cancelBtn.textContent = 'Cancel';
        cancelBtn.style.cssText = 'cursor: pointer;';
        cancelBtn.addEventListener('click', closeOverlay);

        function doSave() {
            currentConfig = {
                perRow: Math.max(1, parseInt(perRowInput.value, 10) || DEFAULT_CONFIG.perRow),
                order: workingOrder,
                enabled: workingEnabled,
                hotkeys: workingHotkeys,
            };
            saveConfig(currentConfig);
            closeOverlay();
            refreshPanel();
        }

        const saveBtn = document.createElement('button');
        saveBtn.textContent = 'Save';
        saveBtn.style.cssText = 'cursor: pointer; font-weight: bold;';
        saveBtn.addEventListener('click', doSave);

        buttonRow.appendChild(resetBtn);
        buttonRow.appendChild(cancelBtn);
        buttonRow.appendChild(saveBtn);
        box.appendChild(buttonRow);

        function onKeyDown(e) {
            if (hotkeyRecordingActive) return; // let the row's own key-capture listener handle this instead
            if (e.key === 'Escape') {
                e.stopPropagation();
                doSave();
            }
        }

        function closeOverlay() {
            hotkeyRecordingActive = false;
            document.removeEventListener('keydown', onKeyDown, true);
            overlay.remove();
        }

        overlay.appendChild(box);
        overlay.addEventListener('click', (e) => {
            if (e.target === overlay) closeOverlay();
        });
        document.addEventListener('keydown', onKeyDown, true);
        document.body.appendChild(overlay);
    }

    function attachGearButton() {
        if (document.getElementById('fmmo-gear-btn')) return; // already attached

        const titleBar = document.querySelector('#ui-panel-inventory .ui-panel-title');
        if (!titleBar) return;

        const gearBtn = document.createElement('span');
        gearBtn.id = 'fmmo-gear-btn';
        gearBtn.className = 'hover';
        gearBtn.textContent = '\u2699\uFE0F';
        gearBtn.title = 'Configure Quick Actions buttons';
        gearBtn.style.cssText =
            'float: right; cursor: pointer; font-size: 12px; vertical-align: middle; margin-right: 6px;';
        gearBtn.addEventListener('click', openSettingsPanel);

        // Appended after the bank icon in the DOM, so with float:right it lands
        // immediately to its left.
        titleBar.appendChild(gearBtn);
    }

    function attachPanel() {
        if (document.getElementById('fmmo-quick-actions')) return; // already attached

        const inventory = document.querySelector(INVENTORY_SELECTOR);
        const panel = buildPanel();

        if (inventory) {
            inventory.insertAdjacentElement('afterend', panel);
        } else {
            panel.style.position = 'fixed';
            panel.style.bottom = '10px';
            panel.style.right = '10px';
            panel.style.zIndex = 9999;
            document.body.appendChild(panel);
            console.warn(
                'FlatMMO+ Quick Actions: inventory panel not found, using floating fallback. ' +
                    'Edit INVENTORY_SELECTOR at the top of the script.'
            );
        }
    }

    function initUI() {
        // The game UI may still be a tick behind onLogin, so retry briefly.
        let attempts = 0;
        const interval = setInterval(() => {
            attempts++;
            if (document.querySelector(INVENTORY_SELECTOR) || attempts > 20) {
                clearInterval(interval);
                attachPanel();
                attachGearButton();
            }
        }, 500);

        // React instantly to worship point changes instead of polling on a timer
        watchWorshipPoints();
    }

    // ---- FlatMMOPlus plugin registration ----
    class QuickActionsPlugin extends FlatMMOPlusPlugin {
        constructor() {
            super('quickActions', {
                about: {
                    name: GM_info.script.name,
                    version: GM_info.script.version,
                    author: GM_info.script.author,
                    description: GM_info.script.description,
                },
            });
        }

        // Called by FlatMMOPlus once login is detected (or immediately on
        // registration if already logged in) - the real hook for "the game UI
        // is ready", replacing the old blind polling-from-page-load approach.
        onLogin() {
            initUI();
        }
    }

    const plugin = new QuickActionsPlugin();
    FlatMMOPlus.registerPlugin(plugin);
})();