remappable keybindings for flatmmo
// ==UserScript==
// @name Flatmmo Keybinds Improved
// @namespace Joshu FlatMMO Scripts
// @description remappable keybindings for flatmmo
// @license MIT
// @match https://flatmmo.com/play.php*
// @version 1773961479750
// @grant GM_getValue
// @grant GM_setValue
// @grant GM.setValue
// @grant GM.registerMenuCommand
// ==/UserScript==
// packages/userscripts/keybinds-improved/ACTIONS.ts
var ACTIONS = {
run: {
originalKey: "F1",
description: "Run",
socketCommand: "SHORTCUT_KEY=F1"
},
eat: {
originalKey: "F2",
description: "Consumes a piece of food",
socketCommand: "SHORTCUT_KEY=F2"
},
lightFire: {
originalKey: "F3",
description: "Lights a fire",
socketCommand: "SHORTCUT_KEY=F3"
},
equip1: {
originalKey: "F6",
description: "Equipment Auto equips items that you've configured",
socketCommand: "SHORTCUT_KEY=F6"
},
equip2: {
originalKey: "F7",
description: "Equipment - Auto equips items that you've configured",
socketCommand: "SHORTCUT_KEY=F7"
},
equip3: {
originalKey: "F8",
description: "Equipment - Auto equips items that you've configured",
socketCommand: "SHORTCUT_KEY=F8"
},
badge1: {
originalKey: "F9",
description: "Badge - Right click a badge and click the 'set key binding'",
socketCommand: "SHORTCUT_KEY=F9"
},
badge2: {
originalKey: "F10",
description: "Badge - Right click a badge and click the 'set key binding'",
socketCommand: "SHORTCUT_KEY=F10"
},
badge3: {
originalKey: "F11",
description: "Badge - Right click a badge and click the 'set key binding'",
socketCommand: "SHORTCUT_KEY=F11"
},
badge4: {
originalKey: "F12",
description: "Badge - Right click a badge and click the 'set key binding'",
socketCommand: "SHORTCUT_KEY=F12"
},
teleport_everbrook: {
originalKey: "N/A",
description: "Worship skill, originally no hotkey",
socketCommand: "USE_WORSHIP=teleport_everbrook"
},
remote_sell: {
originalKey: "N/A",
description: "Worship skill, originally no hotkey",
socketCommand: "USE_WORSHIP=remote_sell"
},
dig: {
originalKey: "N/A",
description: "Worship skill, originally no hotkey",
socketCommand: "USE_WORSHIP=dig"
},
teleport_mysticvale: {
originalKey: "N/A",
description: "Worship skill, originally no hotkey",
socketCommand: "USE_WORSHIP=teleport_mysticvale"
},
timers: {
originalKey: "N/A",
description: "Worship skill, originally no hotkey",
socketCommand: "USE_WORSHIP=timers"
},
teleport_omboko: {
originalKey: "N/A",
description: "Worship skill, originally no hotkey",
socketCommand: "USE_WORSHIP=teleport_omboko"
},
teleport_dock_haven: {
originalKey: "N/A",
description: "Worship skill, originally no hotkey",
socketCommand: "USE_WORSHIP=teleport_dock_haven"
},
auto_hell_burying: {
originalKey: "N/A",
description: "Worship skill, originally no hotkey",
socketCommand: "USE_WORSHIP=auto_hell_burying"
},
teleport_jafa_outpost: {
originalKey: "N/A",
description: "Worship skill, originally no hotkey",
socketCommand: "USE_WORSHIP=teleport_jafa_outpost"
},
teleport_frostvale: {
originalKey: "N/A",
description: "Worship skill, originally no hotkey",
socketCommand: "USE_WORSHIP=teleport_frostvale"
},
hunting_contact: {
originalKey: "N/A",
description: "Worship skill, originally no hotkey",
socketCommand: "USE_WORSHIP=hunting_contact"
},
mass_pickup: {
originalKey: "N/A",
description: "Worship skill, originally no hotkey",
socketCommand: "USE_WORSHIP=mass_pickup"
},
focus: {
originalKey: "N/A",
description: "Worship skill, originally no hotkey",
socketCommand: "USE_WORSHIP=focus"
},
clarity: {
originalKey: "N/A",
description: "Worship skill, originally no hotkey",
socketCommand: "USE_WORSHIP=clarity"
}
};
// packages/userscripts/keybinds-improved/DEFAULT_HOTKEYS.ts
var DEFAULT_HOTKEYS = {
run: {
key: "r",
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false
},
eat: {
key: "f",
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false
},
lightFire: {
key: "4",
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false
},
equip1: {
key: "1",
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false
},
equip2: {
key: "2",
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false
},
equip3: {
key: "3",
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false
},
badge1: {
key: "a",
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false
},
badge2: {
key: "s",
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false
},
badge3: {
key: "d",
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false
},
badge4: {
key: "v",
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false
},
teleport_everbrook: {
key: "e",
altKey: false,
ctrlKey: true,
metaKey: false,
shiftKey: false
},
remote_sell: {
key: "s",
altKey: false,
ctrlKey: true,
metaKey: false,
shiftKey: false
},
dig: {
key: "l",
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false
},
teleport_mysticvale: {
key: "m",
altKey: false,
ctrlKey: true,
metaKey: false,
shiftKey: false
},
timers: {
key: "0",
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false
},
teleport_omboko: {
key: "o",
altKey: false,
ctrlKey: true,
metaKey: false,
shiftKey: false
},
teleport_dock_haven: {
key: "d",
altKey: false,
ctrlKey: true,
metaKey: false,
shiftKey: false
},
auto_hell_burying: {
key: "b",
altKey: false,
ctrlKey: true,
metaKey: false,
shiftKey: false
},
teleport_jafa_outpost: {
key: "j",
altKey: false,
ctrlKey: true,
metaKey: false,
shiftKey: false
},
teleport_frostvale: {
key: "f",
altKey: false,
ctrlKey: true,
metaKey: false,
shiftKey: false
},
hunting_contact: {
key: "h",
altKey: false,
ctrlKey: true,
metaKey: false,
shiftKey: false
},
mass_pickup: {
key: "p",
altKey: false,
ctrlKey: true,
metaKey: false,
shiftKey: false
},
focus: {
key: "k",
altKey: false,
ctrlKey: true,
metaKey: false,
shiftKey: false
},
clarity: {
key: "c",
altKey: false,
ctrlKey: true,
metaKey: false,
shiftKey: false
}
};
// packages/userscripts/keybinds-improved/hotkeys.ts
var keypressToHashableString = (keypress) => {
return `${keypress.key}-${keypress.altKey}-${keypress.ctrlKey}-${keypress.metaKey}-${keypress.shiftKey}`.toLowerCase();
};
if (GM_getValue("hotkeys", null) === null) {
GM_setValue("hotkeys", {});
}
var usersHotkeys = GM_getValue("hotkeys", {});
var mergeHotkeys = () => {
return { ...DEFAULT_HOTKEYS, ...usersHotkeys };
};
var hashKeymap = () => {
return Object.entries(mergeHotkeys()).reduce((result, [action, kp]) => {
const hashed = keypressToHashableString(kp);
result[hashed] = { action, hotkey: kp };
return result;
}, {});
};
var hashedHotkeyMap = hashKeymap();
var setHotkeys = (updatedHotkeys) => {
usersHotkeys = { ...usersHotkeys, ...updatedHotkeys };
GM.setValue("hotkeys", usersHotkeys);
hashedHotkeyMap = hashKeymap();
};
// packages/userscripts/keybinds-improved/settings.ts
var formatKeypress = (kp) => {
const parts = [];
if (kp.ctrlKey)
parts.push("Ctrl");
if (kp.altKey)
parts.push("Alt");
if (kp.shiftKey)
parts.push("Shift");
if (kp.metaKey)
parts.push("Meta");
parts.push(kp.key.toUpperCase());
return parts.join(" + ");
};
var formatKeypressFromEvent = (e) => {
const parts = [];
if (e.ctrlKey)
parts.push("Ctrl");
if (e.altKey)
parts.push("Alt");
if (e.shiftKey)
parts.push("Shift");
if (e.metaKey)
parts.push("Meta");
parts.push(e.key.toUpperCase());
return parts.join(" + ");
};
var formatActionName = (action) => {
return action.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
};
var CATEGORIES = {
"Basic Actions": ["run", "eat", "lightFire"],
Equipment: ["equip1", "equip2", "equip3"],
Badges: ["badge1", "badge2", "badge3", "badge4"],
Teleports: [
"teleport_everbrook",
"teleport_mysticvale",
"teleport_omboko",
"teleport_dock_haven",
"teleport_jafa_outpost",
"teleport_frostvale"
],
"Worship Skills": [
"remote_sell",
"dig",
"timers",
"auto_hell_burying",
"hunting_contact",
"mass_pickup",
"focus",
"clarity"
]
};
var createModalStyles = () => {
const style = document.createElement("style");
style.textContent = `
#hotkeys-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.7);
display: none;
justify-content: center;
z-index: 10000;
padding-top: 5vh;
}
#hotkeys-modal-overlay.visible {
display: flex;
}
#hotkeys-modal {
background: #1a1a2e;
border: 2px solid #4a4a6a;
border-radius: 8px;
max-width: 1000px;
max-height: 80vh;
width: 90%;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
height: fit-content;
padding-bottom: 20px;
}
#hotkeys-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #252540;
border-bottom: 1px solid #4a4a6a;
}
#hotkeys-modal-header h2 {
margin: 0;
color: #e0e0e0;
font-size: 18px;
}
#hotkeys-modal-close {
background: none;
border: none;
color: #888;
font-size: 24px;
cursor: pointer;
padding: 0;
line-height: 1;
}
#hotkeys-modal-close:hover {
color: #fff;
}
#hotkeys-modal-content {
overflow-y: auto;
max-height: calc(80vh - 60px);
padding: 16px;
}
.hotkey-category {
font-size: 16px;
font-weight: 600;
color: #e0e0e0;
padding: 12px 0 8px 0;
border-bottom: 2px solid #4a4a6a;
margin-bottom: 8px;
}
#hotkeys-grid {
display: flex;
flex-direction: column;
gap: 16px;
color: #e0e0e0;
}
.category-section {
display: contents;
}
.category-section.full-width {
display: flex;
flex-direction: column;
gap: 8px;
}
.small-categories-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
}
.small-category-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.full-width .items-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 8px;
}
.small-category-container .items-grid {
display: flex;
flex-direction: column;
gap: 8px;
}
.hotkey-item {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 12px;
padding: 8px 12px;
border: 1px solid #333;
border-radius: 4px;
transition: background 0.15s ease;
}
.hotkey-item:hover {
background: #252540;
}
.hotkey-key {
display: inline-block;
background: #333;
border: 1px solid #555;
border-radius: 4px;
padding: 4px 12px;
font-family: monospace;
font-size: 14px;
cursor: pointer;
transition: all 0.15s ease;
}
.hotkey-key:hover {
background: #444;
border-color: #777;
}
.hotkey-key.recording {
background: #4a3a2e;
border-color: #f0a050;
}
.hotkey-key.conflict {
background: #4a2a2a;
border-color: #c55;
}
`;
return style;
};
var currentlyRecording = null;
var recordingAction = null;
var stopRecording = (restoreText = false) => {
if (currentlyRecording) {
if (restoreText && recordingAction) {
const currentHotkeys = mergeHotkeys();
currentlyRecording.textContent = formatKeypress(currentHotkeys[recordingAction]);
}
currentlyRecording.classList.remove("recording");
currentlyRecording = null;
recordingAction = null;
}
};
var allSpans = [];
var updateConflicts = () => {
const currentHotkeys = mergeHotkeys();
const hashCounts = Object.values(currentHotkeys).map(keypressToHashableString).reduce((acc, hash) => {
acc[hash] = (acc[hash] || 0) + 1;
return acc;
}, {});
for (const span of allSpans) {
const action = span.dataset.action;
const kp = currentHotkeys[action];
const hash = keypressToHashableString(kp);
const count = hashCounts[hash];
if (count > 1) {
span.classList.add("conflict");
} else {
span.classList.remove("conflict");
}
}
};
var startRecording = (element, action) => {
if (currentlyRecording === element) {
stopRecording(true);
return;
}
if (currentlyRecording) {
stopRecording(true);
}
currentlyRecording = element;
recordingAction = action;
element.classList.add("recording");
element.textContent = "Press a key...";
};
var getModifierText = (e) => {
const parts = [];
if (e.ctrlKey)
parts.push("Ctrl");
if (e.altKey)
parts.push("Alt");
if (e.shiftKey)
parts.push("Shift");
if (e.metaKey)
parts.push("Meta");
return parts.length > 0 ? `${parts.join(" + ")} + ...` : "Press a key...";
};
var updateRecordingDisplay = (e) => {
if (!currentlyRecording)
return;
currentlyRecording.textContent = getModifierText(e);
};
var handleRecordingKeypress = (e) => {
if (!currentlyRecording || !recordingAction)
return;
if (["Control", "Alt", "Shift", "Meta"].includes(e.key)) {
updateRecordingDisplay(e);
return;
}
e.preventDefault();
e.stopPropagation();
const newKeypress = {
key: e.key,
altKey: e.altKey,
ctrlKey: e.ctrlKey,
metaKey: e.metaKey,
shiftKey: e.shiftKey
};
setHotkeys({ [recordingAction]: newKeypress });
currentlyRecording.textContent = formatKeypressFromEvent(e);
currentlyRecording.classList.remove("recording");
updateConflicts();
stopRecording();
};
var handleRecordingKeyup = (e) => {
if (!currentlyRecording)
return;
if (["Control", "Alt", "Shift", "Meta"].includes(e.key)) {
updateRecordingDisplay(e);
}
};
var createModal = () => {
const overlay = document.createElement("div");
overlay.id = "hotkeys-modal-overlay";
const modal = document.createElement("div");
modal.id = "hotkeys-modal";
const header = document.createElement("div");
header.id = "hotkeys-modal-header";
const title = document.createElement("h2");
title.textContent = "Hotkey Bindings";
const closeBtn = document.createElement("button");
closeBtn.id = "hotkeys-modal-close";
closeBtn.textContent = "×";
closeBtn.addEventListener("click", () => hideModal());
header.appendChild(title);
header.appendChild(closeBtn);
const content = document.createElement("div");
content.id = "hotkeys-modal-content";
const grid = document.createElement("div");
grid.id = "hotkeys-grid";
const hotkeys = mergeHotkeys();
const smallCategories = [];
const largeCategories = [];
for (const [categoryName, categoryActions] of Object.entries(CATEGORIES)) {
if (categoryActions.length <= 4) {
smallCategories.push([categoryName, categoryActions]);
} else {
largeCategories.push([categoryName, categoryActions]);
}
}
const createCategoryItems = (categoryActions) => {
const items = [];
for (const action of categoryActions) {
const kp = hotkeys[action];
const actionInfo = ACTIONS[action];
if (!actionInfo || !kp)
continue;
const item = document.createElement("div");
item.className = "hotkey-item";
const actionName = document.createElement("span");
actionName.className = "hotkey-action";
actionName.textContent = formatActionName(action);
const hotkeySpan = document.createElement("span");
hotkeySpan.className = "hotkey-key";
hotkeySpan.textContent = formatKeypress(kp);
hotkeySpan.dataset.action = action;
allSpans.push(hotkeySpan);
hotkeySpan.addEventListener("click", () => {
startRecording(hotkeySpan, action);
});
item.appendChild(actionName);
item.appendChild(hotkeySpan);
items.push(item);
}
return items;
};
if (smallCategories.length > 0) {
const smallCategoriesRow = document.createElement("div");
smallCategoriesRow.className = "small-categories-row";
for (const [categoryName, categoryActions] of smallCategories) {
const container = document.createElement("div");
container.className = "small-category-container";
const categoryHeader = document.createElement("div");
categoryHeader.className = "hotkey-category";
categoryHeader.textContent = categoryName;
container.appendChild(categoryHeader);
const itemsGrid = document.createElement("div");
itemsGrid.className = "items-grid";
const items = createCategoryItems(categoryActions);
items.forEach((item) => itemsGrid.appendChild(item));
container.appendChild(itemsGrid);
smallCategoriesRow.appendChild(container);
}
grid.appendChild(smallCategoriesRow);
}
for (const [categoryName, categoryActions] of largeCategories) {
const section = document.createElement("div");
section.className = "category-section full-width";
const categoryHeader = document.createElement("div");
categoryHeader.className = "hotkey-category";
categoryHeader.textContent = categoryName;
section.appendChild(categoryHeader);
const itemsGrid = document.createElement("div");
itemsGrid.className = "items-grid";
const items = createCategoryItems(categoryActions);
items.forEach((item) => itemsGrid.appendChild(item));
section.appendChild(itemsGrid);
grid.appendChild(section);
}
updateConflicts();
content.appendChild(grid);
modal.appendChild(header);
modal.appendChild(content);
overlay.appendChild(modal);
overlay.addEventListener("click", (e) => {
if (e.target === overlay) {
hideModal();
}
});
modal.addEventListener("click", (e) => {
const target = e.target;
if (!target.classList.contains("hotkey-key") && currentlyRecording) {
stopRecording(true);
}
});
return overlay;
};
var modalElement = null;
var initModal = () => {
if (modalElement)
return;
document.head.appendChild(createModalStyles());
modalElement = createModal();
document.body.appendChild(modalElement);
};
var showModal = () => {
initModal();
modalElement?.classList.add("visible");
};
var hideModal = () => {
modalElement?.classList.remove("visible");
};
var toggleModal = () => {
initModal();
if (modalElement?.classList.contains("visible")) {
hideModal();
} else {
showModal();
}
};
document.addEventListener("keydown", (e) => {
if (!modalElement?.classList.contains("visible"))
return;
if (currentlyRecording) {
if (e.key === "Escape") {
stopRecording(true);
e.preventDefault();
return;
}
handleRecordingKeypress(e);
return;
}
if (e.key === "Escape") {
hideModal();
e.preventDefault();
}
});
document.addEventListener("keyup", (e) => {
if (!modalElement?.classList.contains("visible"))
return;
handleRecordingKeyup(e);
});
// packages/userscripts/keybinds-improved/index.ts
var focusOrSendChat = () => {
const value = chat_ele.value.trim();
if (document.activeElement !== chat_ele) {
request_focus_chatbox();
return;
}
if (value !== "") {
Globals.websocket.send(`CHAT=${value}`);
chat_ele.value = "";
}
request_unfocus_chatbox();
};
var handleNpcChatModal = (e) => {
const keyCode = e.keyCode;
if (has_npc_chat_message_modal_open()) {
if (keyCode === 32) {
document.getElementById("npc-chat-message-modal-continue-btn")?.click();
e.preventDefault();
}
return;
}
if (has_npc_chat_options_modal_open()) {
switch (keyCode) {
case 49:
{
const wrapper = document.getElementById("npc-chat-options-modal-content");
const options = wrapper?.getElementsByTagName("div");
if (options && options[0].style.display !== "none") {
options[0].click();
}
}
break;
case 50:
{
const wrapper = document.getElementById("npc-chat-options-modal-content");
const options = wrapper?.getElementsByTagName("div");
if (options && options[1].style.display !== "none") {
options[1].click();
}
}
break;
case 51:
{
const wrapper = document.getElementById("npc-chat-options-modal-content");
const options = wrapper?.getElementsByTagName("div");
if (options && options[2].style.display !== "none") {
options[2].click();
}
}
break;
case 52:
{
const wrapper = document.getElementById("npc-chat-options-modal-content");
const options = wrapper?.getElementsByTagName("div");
if (options && options[3].style.display !== "none") {
options[3].click();
}
}
break;
}
}
};
var hotkeyListener = (e) => {
if (e.repeat)
return;
if (Globals.local_username == null)
return;
if (has_npc_chat_message_modal_open()) {
handleNpcChatModal(e);
return;
}
if (has_modal_open())
return;
if (e.key === "Enter") {
focusOrSendChat();
e.preventDefault();
}
if (document.activeElement?.id !== "body") {
return;
}
if (e.key === "/") {
chat_ele.value = "/";
request_focus_chatbox();
e.preventDefault();
}
const keypressString = keypressToHashableString(e);
if (keypressString in hashedHotkeyMap) {
const pressedHotkey = hashedHotkeyMap[keypressString];
const action = ACTIONS[pressedHotkey.action];
if (action) {
Globals.websocket.send(action.socketCommand);
e.preventDefault();
}
}
};
GM.registerMenuCommand("Toggle Settings menu", toggleModal);
window.removeEventListener("keypress", keypress_listener);
window.addEventListener("keydown", hotkeyListener, false);