Global Keyboard Hotkey + Crafting QoL for SMMO

Hotkeys for navigating Simple MMO: 'H' to go Home, 'T' to Town, 'I' to Inventory, 'B' to Battle Menu, 'Q' to Quests, etc.

安裝腳本?
作者推薦腳本

您可能也會喜歡 Inventory Keyboard Hotkey for SMMO

安裝腳本
// ==UserScript==
// @name         Global Keyboard Hotkey + Crafting QoL for SMMO
// @namespace    http://tampermonkey.net/
// @version      1.1.0
// @description  Hotkeys for navigating Simple MMO: 'H' to go Home, 'T' to Town, 'I' to Inventory, 'B' to Battle Menu, 'Q' to Quests, etc.
// @author       @dngda
// @match        https://web.simple-mmo.com/*
// @exclude      https://web.simple-mmo.com/chat*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=simple-mmo.com
// @grant        none
// @license      GNU GPLv3
// ==/UserScript==

(() => {
    "use strict";

    let user = {};

    function getInfo() {
        fetch("https://web.simple-mmo.com/api/web-app")
            .then((response) => response.json())
            .then((data) => {
                user = data;
                const badge = document.getElementById("info-badge");

                if (badge) {
                    badge.innerHTML = `EP <span class="text-indigo-600 font-semibold">${user.energy}</span><span class="text-gray-600">/${user.max_energy}</span>
                     &emsp; QP <span class="text-indigo-600 font-semibold">${user.quest_points}</span><span class="text-gray-600">/${user.max_quest_points}</span>`;
                }
            })
            .catch((error) => {
                console.error("Error fetching user data:", error);
            });
    }

    // ================== UTIL: INPUT STATE ==================
    function isTypingElement() {
        const ae = document.activeElement;
        if (!ae) return false;
        const tag = (ae.tagName || "").toLowerCase();

        const type = (ae.type || "").toLowerCase();
        if (tag === "input") {
            const nonTypingTypes = [
                "button",
                "checkbox",
                "radio",
                "submit",
                "reset",
                "hidden",
                "image",
            ];
            if (nonTypingTypes.includes(type)) {
                return false;
            }
        }
        return (
            ["input", "textarea", "select"].includes(tag) ||
            !!ae.isContentEditable
        );
    }

    // ================== HOTKEYS ==================
    function attachHotkeys() {
        if (!user.id) {
            user.id = document
                .querySelector("a[href*=collection")
                .getAttribute("href")
                .split("/")[2];
        }

        document.addEventListener(
            "keydown",
            (e) => {
                if (isTypingElement()) return;
                const singleKey =
                    !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey;

                if (e.code === "KeyH" && singleKey) {
                    e.preventDefault();
                    location.href = "/home";
                }

                if (e.code === "KeyT" && singleKey) {
                    e.preventDefault();
                    location.href = "/town";
                }

                if (e.code === "KeyI" && singleKey) {
                    e.preventDefault();
                    // Check if there's a saved inventory URL
                    const savedInventoryUrl =
                        localStorage.getItem("tm_saved_inv_url");
                    if (savedInventoryUrl) {
                        location.href = savedInventoryUrl;
                    } else {
                        location.href = "/inventory/items";
                    }
                }

                if (e.code === "KeyI" && e.shiftKey) {
                    e.preventDefault();
                    location.href = "/inventory/storage";
                }

                if (e.code === "KeyB" && singleKey) {
                    e.preventDefault();
                    location.href = "/battle/menu";
                }

                if (e.code === "KeyB" && e.shiftKey) {
                    e.preventDefault();
                    location.href = "/battle/arena";
                }

                if (e.code === "KeyQ" && singleKey) {
                    e.preventDefault();
                    location.href = "/quests";
                }

                if (e.code === "KeyX" && singleKey) {
                    e.preventDefault();
                    location.href = "/character";
                }

                if (e.code === "KeyF" && singleKey) {
                    e.preventDefault();
                    location.href = "/profession";
                }

                if (e.code === "KeyR" && singleKey) {
                    e.preventDefault();
                    location.href = "/crafting/menu";
                }

                if (e.code === "KeyK" && singleKey) {
                    e.preventDefault();
                    location.href = "/tasks/viewall";
                }

                if (e.code === "KeyM" && singleKey) {
                    e.preventDefault();
                    location.href = "/market/listings?type[]=";
                }

                if (e.code === "KeyL" && singleKey) {
                    e.preventDefault();
                    location.href = `/market/listings?user_id=${user.id}`;
                }

                if (e.code === "KeyE" && singleKey) {
                    e.preventDefault();
                    location.href = "/events";
                }

                if (e.code === "KeyP" && singleKey) {
                    e.preventDefault();
                    location.href = `/user/view/${user.id}`;
                }

                if (e.code === "Slash" && e.shiftKey) {
                    e.preventDefault();

                    const windowName = "travelWindow";
                    const fullHeight = window.innerHeight;
                    const left = screen.width - 546;
                    const windowFeatures = `popup,width=546,height=${fullHeight},left=${left}`;

                    // Try to focus existing window first
                    try {
                        if (
                            window.travelWindowRef &&
                            !window.travelWindowRef.closed
                        ) {
                            // Check if it's already on travel page
                            if (
                                window.travelWindowRef.location.href.includes(
                                    "/travel"
                                )
                            ) {
                                window.travelWindowRef.focus();
                                return;
                            }
                        }
                    } catch (e) {
                        // Reference lost or cross-origin issue
                    }

                    // Open empty window to get reference, then navigate
                    window.travelWindowRef = window.open(
                        "",
                        windowName,
                        windowFeatures
                    );

                    if (window.travelWindowRef) {
                        try {
                            // Check if window needs to navigate to travel
                            if (
                                !window.travelWindowRef.location.href.includes(
                                    "/travel"
                                )
                            ) {
                                window.travelWindowRef.location.href =
                                    "https://web.simple-mmo.com/travel";
                            }
                        } catch (e) {
                            // If we can't check href, just navigate
                            window.travelWindowRef.location.href =
                                "https://web.simple-mmo.com/travel";
                        }
                        window.travelWindowRef.focus();
                    }
                }
            },
            { passive: false }
        );
    }

    // ================== Crafting Countdown ==================

    function saveCraftingTime(progressText) {
        // 0d 0h 0m 0s
        const match = progressText.match(/(\d+)d\s+(\d+)h\s+(\d+)m\s+(\d+)s/);
        if (match) {
            const days = parseInt(match[1], 10);
            const hours = parseInt(match[2], 10);
            const minutes = parseInt(match[3], 10);
            const seconds = parseInt(match[4], 10);
            const totalSeconds =
                days * 86400 + hours * 3600 + minutes * 60 + seconds;

            // Simpan waktu selesai ke localStorage
            const endTime = Date.now() + totalSeconds * 1000;
            try {
                localStorage.setItem("craftingEndTime", endTime.toString());
                console.log("Crafting time saved:", totalSeconds, "seconds");
            } catch {
                alert(
                    "Error accessing localStorage. Crafting time state cannot be persisted."
                );
            }

            // Trigger update countdown di semua halaman
            displayCraftingCountdown();
        }
    }

    function displayCraftingCountdown() {
        // Create countdown element di menu
        const craftBtn = Array.from(
            document.querySelectorAll("a[href*='/crafting/menu']")
        ).find(isVisibleElement);
        if (!craftBtn) return;

        // Hapus countdown lama jika ada
        const oldCountdown = document.getElementById("crafting-countdown");
        if (oldCountdown) oldCountdown.remove();

        const countdown = document.createElement("div");
        countdown.id = "crafting-countdown";
        countdown.style.cssText = [
            "margin-left:8px",
            "font-size:12px",
            "color:black",
            "background:#00ff00",
            "padding:2px 4px",
            "border-radius:4px",
            "opacity:.9",
        ].join(";");
        countdown.textContent = "Ready!";
        craftBtn.children[1].after(countdown);

        // Ambil waktu selesai dari localStorage
        const savedEndTime = localStorage.getItem("craftingEndTime");
        if (!savedEndTime) return;

        const endTime = parseInt(savedEndTime, 10);
        const now = Date.now();

        // Jika sudah selesai, hapus dari localStorage
        if (now >= endTime) {
            localStorage.removeItem("craftingEndTime");
            return;
        }

        // Start the countdown
        const updateCountdown = () => {
            const remaining = Math.floor((endTime - Date.now()) / 1000);

            if (remaining <= 0) {
                countdown.textContent = "Ready!";
                countdown.style.background = "#00ff00";
                localStorage.removeItem("craftingEndTime");
                return;
            }

            const mins = Math.floor(remaining / 60);
            const secs = remaining % 60;
            countdown.style.background = "#ffff00";
            countdown.textContent = `${mins}:${secs < 10 ? "0" : ""}${secs}`;

            setTimeout(updateCountdown, 1000);
        };

        updateCountdown();
    }

    function setupCraftingObserver() {
        if (location.pathname !== "/crafting/menu") return;

        let isChecking = true;
        const checkProgress = () => {
            const progressEl = document.querySelector(
                'div[x-text="current_crafting_session.progress_text"]'
            );

            if (progressEl && progressEl.textContent) {
                const text = progressEl.textContent.trim();
                if (text.match(/\d+d\s+\d+h\s+\d+m\s+\d+s/)) {
                    saveCraftingTime(text);
                    isChecking = false;
                }
            }
        };

        setInterval(() => {
            if (!isChecking) return;
            checkProgress();
        }, 1000);
    }

    function createBadge(id) {
        const bid = id;
        let badge = document.getElementById(bid);
        if (!badge) {
            badge = document.createElement("div");
            badge.id = bid;
            badge.style.cssText = [
                "position:fixed",
                "left:8px",
                "bottom:8px",
                "z-index:1001",
                "background:#4f46e5",
                "color:#fff",
                "padding:6px 8px",
                "font-size:12px",
                "border-radius:6px",
                "opacity:.9",
                "font-family:sans-serif",
                "cursor:help",
            ].join(";");
            document.body.appendChild(badge);
        }

        return badge;
    }

    // ================== Save Inventory URL ==================
    function createSaveInventoryButton() {
        // Only show on inventory pages
        if (!location.pathname.includes("/inventory")) return;

        // Check if button already exists
        if (document.getElementById("save-inventory-btn")) return;

        const button = document.createElement("button");
        button.id = "save-inventory-btn";
        button.textContent = "Save Inv URL";
        button.style.cssText = [
            "position:fixed",
            "left:8px",
            "bottom:80px",
            "z-index:1001",
            "background:#10b981",
            "color:#fff",
            "padding:6px 12px",
            "font-size:12px",
            "border-radius:6px",
            "border:none",
            "cursor:pointer",
            "font-family:sans-serif",
            "font-weight:500",
            "transition:background 0.2s",
        ].join(";");

        button.addEventListener("mouseenter", () => {
            button.style.background = "#059669";
        });

        button.addEventListener("mouseleave", () => {
            button.style.background = "#10b981";
        });

        button.addEventListener("click", () => {
            const currentUrl = location.pathname + location.search;
            localStorage.setItem("tm_saved_inv_url", currentUrl);

            // Visual feedback
            const originalText = button.textContent;
            button.textContent = "✓ Saved!";
            button.style.background = "#6366f1";

            setTimeout(() => {
                button.textContent = originalText;
                button.style.background = "#10b981";
            }, 1500);
        });

        document.body.appendChild(button);
    }

    function isVisibleElement(el) {
        const cs = window.getComputedStyle(el);
        const r = el.getBoundingClientRect();
        if (cs.display === "none" || cs.visibility === "hidden") return false;
        if (r.width === 0 && r.height === 0) return false;
        return true;
    }

    function addHint(selector, text) {
        const el = Array.from(document.querySelectorAll(selector)).find(
            isVisibleElement
        );
        if (!el) return;
        const kbd = document.createElement("kbd");
        kbd.style.marginLeft = "auto";
        kbd.textContent = text;
        kbd.style.color = "yellow";
        el.appendChild(kbd);
    }

    // ================== Info ==================
    function ensureUI() {
        addHint("a[href='/home']", "H");
        addHint("a[href='/town']", "T");
        addHint("a[href*='/inventory']", "I");
        addHint("a[href*='/battle/menu']", "B");
        addHint("a[href*='/quests']", "Q");

        addHint("a[href*='/character']", "X");
        addHint("a[href*='/profession']", "F");
        addHint("a[href*='/crafting/menu']", "R");
        addHint("a[href*='/tasks/viewall']", "K");

        // Badge hotkey info
        const badge = createBadge("hotkey-badge");
        badge.innerHTML = "Hover for other Hotkeys ⓘ";

        // Create tooltip
        const tooltip = document.createElement("div");
        tooltip.id = "hotkey-tooltip";
        tooltip.style.cssText = [
            "position:fixed",
            "left:8px",
            "bottom:44px",
            "z-index:1005",
            "background:#1f2937",
            "color:#fff",
            "padding:12px",
            "font-size:11px",
            "border-radius:6px",
            "font-family:monospace",
            "line-height:1.6",
            "display:none",
            "white-space:pre",
            "box-shadow:0 4px 6px rgba(0,0,0,0.3)",
        ].join(";");

        tooltip.textContent = [
            "Other Hotkeys:",
            "[P] Profile",
            "[M] Market",
            "[L] My Listings",
            "[E] Notifications",
            "[Shift+B] Battle Arena",
            "[Shift+I] Inventory Storage",
            "[Shift+/] Mini Travel Window",
        ].join("\n");

        document.body.appendChild(tooltip);

        // Show/hide tooltip on hover
        badge.addEventListener("mouseenter", () => {
            tooltip.style.display = "block";
        });

        badge.addEventListener("mouseleave", () => {
            tooltip.style.display = "none";
        });

        const infoBadge = createBadge("info-badge");
        infoBadge.style.background = "#2a261fff";
        infoBadge.style.bottom = "44px";
        infoBadge.innerHTML = "Fetching...";
    }

    if (
        !location.pathname.includes("/travel") &&
        !location.pathname.includes("/gather")
    ) {
        ensureUI();
        getInfo();
    }

    // Add save inventory button on inventory pages
    createSaveInventoryButton();

    attachHotkeys();
    // Setup crafting observer jika di halaman crafting
    setupCraftingObserver();
    // Display countdown jika ada
    displayCraftingCountdown();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址