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