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