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.

// ==UserScript==
// @name         Global Keyboard Hotkey + Crafting QoL for SMMO
// @namespace    http://tampermonkey.net/
// @version      1.1.2
// @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 infoBadge = document.getElementById("info-badge");

                if (infoBadge) {
                    infoBadge.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>`;
                }

                const timeBadge = document.getElementById("time-badge");
                if (timeBadge) {
                    timeBadge.innerHTML = user.server_time;
                }
            })
            .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();
                    location.href = "/inventory/items";
                }

                if (e.code === "KeyI" && e.shiftKey) {
                    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 === "KeyS" && 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 - 648;
                    const windowFeatures = `popup,width=648,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);
    }

    // ================== REUSABLE UI HELPERS ==================

    /**
     * Create a fixed positioned element with custom styles
     * @param {Object} config - Configuration object
     * @param {string} config.id - Element ID
     * @param {string} config.tag - HTML tag (default: 'div')
     * @param {string} config.content - innerHTML or textContent
     * @param {Object} config.styles - CSS styles object
     * @param {Object} config.events - Event listeners object {eventName: handler}
     * @param {boolean} config.checkExisting - Return existing element if found (default: true)
     * @returns {HTMLElement}
     */
    function createFixedElement(config) {
        const {
            id,
            tag = "div",
            content = "",
            styles = {},
            events = {},
            checkExisting = true,
        } = config;

        // Check if element already exists
        if (checkExisting && id) {
            const existing = document.getElementById(id);
            if (existing) return existing;
        }

        const element = document.createElement(tag);
        if (id) element.id = id;

        // Apply styles
        const styleArray = Object.entries(styles).map(
            ([key, value]) => `${key}:${value}`
        );
        element.style.cssText = styleArray.join(";");

        // Set content
        if (content) {
            if (content.includes("<")) {
                element.innerHTML = content;
            } else {
                element.textContent = content;
            }
        }

        // Attach event listeners
        Object.entries(events).forEach(([event, handler]) => {
            element.addEventListener(event, handler);
        });

        document.body.appendChild(element);
        return element;
    }

    /**
     * Create a badge element (legacy wrapper for backwards compatibility)
     */
    function createBadge(id) {
        return createFixedElement({
            id,
            tag: "div",
            styles: {
                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",
            },
        });
    }

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

        const button = createFixedElement({
            id: "save-inventory-btn",
            tag: "button",
            content: "Save Inv URL",
            styles: {
                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",
            },
            events: {
                mouseenter: (e) => {
                    e.target.style.background = "#059669";
                },
                mouseleave: (e) => {
                    e.target.style.background = "#10b981";
                },
                click: (e) => {
                    const currentUrl = location.pathname + location.search;
                    localStorage.setItem("tm_saved_inv_url", currentUrl);

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

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

        return 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 = createFixedElement({
            id: "hotkey-tooltip",
            content: [
                "Other Hotkeys:",
                "[P] Profile",
                "[M] Market",
                "[L] My Listings",
                "[E] Notifications",
                "[Shift+B] Battle Arena",
                "[Shift+I] Inventory Saved URL",
                "[Shift+S] Inventory > Storage",
                "[Shift+/] Mini Travel Window",
            ].join("\n"),
            styles: {
                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)",
            },
        });

        // 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...";

        const timeBadge = createBadge("time-badge");
        timeBadge.style.background = "#2a261fff";
        timeBadge.style.bottom = "80px";
        timeBadge.title = "Server Time";
        timeBadge.innerHTML = "...";
    }

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