Keybindings

Adds keybindings to Melvor Idle. Visit the Settings menu (X) to view all keybinds.

  1. // ==UserScript==
  2. // @name Keybindings
  3. // @description Adds keybindings to Melvor Idle. Visit the Settings menu (X) to view all keybinds.
  4. // @version 1.1.1
  5. // @license MIT
  6. // @match https://*.melvoridle.com/*
  7. // @exclude https://wiki.melvoridle.com*
  8. // @grant none
  9. // @namespace https://github.com/ChaseStrackbein/melvor-keybindings
  10. // ==/UserScript==
  11.  
  12. window.kb = (() => {
  13. const invalidKeys = ['SHIFT', 'CONTROL', 'ALT', 'META'];
  14. let cachePage = window.currentPage;
  15. let previousPage = -1;
  16. let settingsGrid = null;
  17. let bindingBeingRemapped = null;
  18.  
  19. let keymap = {};
  20.  
  21. const keybindings = [
  22. {
  23. name: 'Menu',
  24. category: 'General',
  25. defaultKeys: { key: 'M' },
  26. callback: () => document.getElementById('page-header-user-dropdown').click()
  27. },
  28. {
  29. name: 'Save',
  30. category: 'General',
  31. defaultKeys: { key: 'S', ctrlKey: true },
  32. callback: () => forceSync(false, false)
  33. },
  34. {
  35. name: 'Reload / Character Select',
  36. category: 'General',
  37. defaultKeys: { key: 'F5' },
  38. callback: () => window.location.reload()
  39. },
  40. {
  41. name: 'Open Wiki',
  42. category: 'General',
  43. defaultKeys: { key: 'F1' },
  44. callback: () => window.open('https://wiki.melvoridle.com/', '_blank')
  45. },
  46. {
  47. name: 'Settings',
  48. category: 'General',
  49. defaultKeys: { key: 'X' },
  50. callback: () => changePage(CONSTANTS.page.Settings, false, false)
  51. },
  52. {
  53. name: 'Shop',
  54. category: 'General',
  55. defaultKeys: { key: 'V' },
  56. callback: () => changePage(CONSTANTS.page.Shop, false, false)
  57. },
  58. {
  59. name: 'Bank',
  60. category: 'General',
  61. defaultKeys: { key: 'B' },
  62. callback: () => changePage(CONSTANTS.page.Bank, false, false)
  63. },
  64. {
  65. name: 'Combat',
  66. category: 'General',
  67. defaultKeys: { key: 'C' },
  68. callback: () => changePage(CONSTANTS.page.Combat, false, false)
  69. },
  70. {
  71. name: 'Eat Equipped Food',
  72. category: 'General',
  73. defaultKeys: { key: 'H' },
  74. callback: () => player.eatFood()
  75. },
  76. {
  77. name: 'Loot All (Combat)',
  78. category: 'General',
  79. defaultKeys: { key: 'SPACE' },
  80. callback: () => combatManager.loot.lootAll()
  81. },
  82. {
  83. name: 'Run (Combat)',
  84. category: 'General',
  85. defaultKeys: { key: 'SPACE', ctrlKey: true },
  86. callback: () => combatManager.stopCombat()
  87. },
  88. {
  89. name: 'Equipment Set 1',
  90. category: 'General',
  91. defaultKeys: { key: '1', ctrlKey: true },
  92. callback: () => player.changeEquipmentSet(0)
  93. },
  94. {
  95. name: 'Equipment Set 2',
  96. category: 'General',
  97. defaultKeys: { key: '2', ctrlKey: true },
  98. callback: () => player.changeEquipmentSet(1)
  99. },
  100. {
  101. name: 'Equipment Set 3',
  102. category: 'General',
  103. defaultKeys: { key: '3', ctrlKey: true },
  104. callback: () => player.changeEquipmentSet(2)
  105. },
  106. {
  107. name: 'Equipment Set 4',
  108. category: 'General',
  109. defaultKeys: { key: '4', ctrlKey: true },
  110. callback: () => player.changeEquipmentSet(3)
  111. },
  112. {
  113. name: 'Search Bank',
  114. category: 'General',
  115. defaultKeys: { key: 'F', ctrlKey: true },
  116. callback: () => {
  117. changePage(CONSTANTS.page.Bank, false, false);
  118. updateBankSearchArray();
  119. document.getElementById('searchTextbox').focus();
  120. }
  121. },
  122. {
  123. name: 'Summoning Synergies Menu',
  124. category: 'General',
  125. defaultKeys: { key: 'S' },
  126. callback: () => {
  127. const modal = $('#modal-summoning-synergy').data('bs.modal');
  128. if (!modal || !modal._isShown) openSynergiesBreakdown();
  129. else modal.hide();
  130. }
  131. },
  132. {
  133. name: 'Search Summoning Synergies',
  134. category: 'General',
  135. defaultKeys: { key: 'F', ctrlKey: true, altKey: true },
  136. callback: () => {
  137. openSynergiesBreakdown();
  138. document.getElementById('summoning-synergy-search').focus();
  139. }
  140. },
  141. {
  142. name: 'Woodcutting',
  143. category: 'General',
  144. defaultKeys: { key: '1' },
  145. callback: () => changePage(CONSTANTS.page.Woodcutting, false, false)
  146. },
  147. {
  148. name: 'Fishing',
  149. category: 'General',
  150. defaultKeys: { key: '2' },
  151. callback: () => changePage(CONSTANTS.page.Fishing, false, false)
  152. },
  153. {
  154. name: 'Firemaking',
  155. category: 'General',
  156. defaultKeys: { key: '3' },
  157. callback: () => changePage(CONSTANTS.page.Firemaking, false, false)
  158. },
  159. {
  160. name: 'Cooking',
  161. category: 'General',
  162. defaultKeys: { key: '4' },
  163. callback: () => changePage(CONSTANTS.page.Cooking, false, false)
  164. },
  165. {
  166. name: 'Mining',
  167. category: 'General',
  168. defaultKeys: { key: '5' },
  169. callback: () => changePage(CONSTANTS.page.Mining, false, false)
  170. },
  171. {
  172. name: 'Smithing',
  173. category: 'General',
  174. defaultKeys: { key: '6' },
  175. callback: () => changePage(CONSTANTS.page.Smithing, false, false)
  176. },
  177. {
  178. name: 'Thieving',
  179. category: 'General',
  180. defaultKeys: { key: '7' },
  181. callback: () => changePage(CONSTANTS.page.Thieving, false, false)
  182. },
  183. {
  184. name: 'Farming',
  185. category: 'General',
  186. defaultKeys: { key: '8' },
  187. callback: () => changePage(CONSTANTS.page.Farming, false, false)
  188. },
  189. {
  190. name: 'Fletching',
  191. category: 'General',
  192. defaultKeys: { key: '9' },
  193. callback: () => changePage(CONSTANTS.page.Fletching, false, false)
  194. },
  195. {
  196. name: 'Crafting',
  197. category: 'General',
  198. defaultKeys: { key: '0' },
  199. callback: () => changePage(CONSTANTS.page.Crafting, false, false)
  200. },
  201. {
  202. name: 'Runecrafting',
  203. category: 'General',
  204. defaultKeys: { key: '!' },
  205. callback: () => changePage(CONSTANTS.page.Runecrafting, false, false)
  206. },
  207. {
  208. name: 'Herblore',
  209. category: 'General',
  210. defaultKeys: { key: '@' },
  211. callback: () => changePage(CONSTANTS.page.Herblore, false, false)
  212. },
  213. {
  214. name: 'Agility',
  215. category: 'General',
  216. defaultKeys: { key: '#' },
  217. callback: () => changePage(CONSTANTS.page.Agility, false, false)
  218. },
  219. {
  220. name: 'Summoning',
  221. category: 'General',
  222. defaultKeys: { key: '$' },
  223. callback: () => changePage(CONSTANTS.page.Summoning, false, false)
  224. },
  225. {
  226. name: 'Astrology',
  227. category: 'General',
  228. defaultKeys: { key: '%' },
  229. callback: () => changePage(CONSTANTS.page.Astrology, false, false)
  230. },
  231. {
  232. name: 'Alt. Magic',
  233. category: 'General',
  234. defaultKeys: { key: 'M', altKey: true },
  235. callback: () => changePage(CONSTANTS.page.AltMagic, false, false)
  236. },
  237. {
  238. name: 'Completion Log',
  239. category: 'General',
  240. defaultKeys: { key: 'Y' },
  241. callback: () => changePage(30, false, false)
  242. },
  243. {
  244. name: 'Statistics',
  245. category: 'General',
  246. defaultKeys: { key: 'F2' },
  247. callback: () => changePage(CONSTANTS.page.Statistics, false, false)
  248. },
  249. {
  250. name: 'Golbin Raid',
  251. category: 'General',
  252. defaultKeys: { key: 'G' },
  253. callback: () => changePage(CONSTANTS.page.GolbinRaid, false, false)
  254. },
  255. {
  256. name: 'Previous Page',
  257. category: 'General',
  258. defaultKeys: { key: 'BACKSPACE' },
  259. callback: () => changePage(previousPage, false, false)
  260. }
  261. ];
  262.  
  263. const createHeader = () => {
  264. const header = document.createElement('h2');
  265. header.classList.add('content-heading', 'border-bottom', 'mb-4', 'pb-2');
  266. header.innerHTML = 'Keybindings';
  267. return header;
  268. };
  269.  
  270. const createHelpText = () => {
  271. const helpText = document.createElement('div');
  272. helpText.classList.add('font-size-sm', 'text-muted', 'ml-2', 'mb-2');
  273. helpText.innerHTML = 'Click a keybinding to remap to new keys.<br />ESC or click again to cancel remapping.<br />CTRL + ALT + SPACE to clear the mapping.';
  274. return helpText;
  275. };
  276. const createWrapper = (grid) => {
  277. const row = document.createElement('div');
  278. row.classList.add('row');
  279. const column = document.createElement('div');
  280. column.classList.add('col-md-6', 'offset-md-3');
  281. const wrapper = document.createElement('div');
  282. wrapper.classList.add('mb-4');
  283. wrapper.appendChild(createHelpText());
  284. wrapper.appendChild(grid);
  285. wrapper.appendChild(createResetButton());
  286. column.appendChild(wrapper);
  287. row.appendChild(column);
  288. return row;
  289. };
  290. const createGrid = () => {
  291. const grid = document.createElement('div');
  292. grid.classList.add('mkb-grid');
  293. return grid;
  294. };
  295. const createRow = (keybinding) => {
  296. const row = document.createElement('div');
  297. row.classList.add('mkb-row');
  298.  
  299. row.addEventListener('click', () => beginListeningForRemap(keybinding));
  300. const nameCell = createCell(keybinding.name);
  301. const keyCell = createCell(keybinding.keys);
  302. row.appendChild(nameCell);
  303. row.appendChild(keyCell);
  304.  
  305. keybinding.keyCell = keyCell;
  306.  
  307. return row;
  308. };
  309. const createCell = (keysOrText) => {
  310. const cell = document.createElement('div');
  311. cell.classList.add('mkb-cell');
  312. if (typeof keysOrText === 'string') cell.innerHTML = keysOrText;
  313. else {
  314. if (keysOrText.ctrlKey) {
  315. cell.appendChild(createKbd('CTRL'));
  316. cell.appendChild(createPlus());
  317. }
  318. if (keysOrText.altKey) {
  319. cell.appendChild(createKbd('ALT'));
  320. cell.appendChild(createPlus());
  321. }
  322. if (keysOrText.key) cell.appendChild(createKbd(keysOrText.key));
  323. }
  324. return cell;
  325. };
  326. const createKbd = (text) => {
  327. const kbd = document.createElement('kbd');
  328. kbd.innerHTML = text;
  329. return kbd;
  330. };
  331. const createPlus = () => {
  332. const plus = document.createTextNode('+');
  333. return plus;
  334. };
  335.  
  336. const createResetButton = () => {
  337. const resetButton = document.createElement('button');
  338. resetButton.type = 'button';
  339. resetButton.classList.add('btn', 'btn-sm', 'btn-danger', 'm-1');
  340. resetButton.innerHTML = 'Reset Default Keybindings';
  341. resetButton.addEventListener('click', resetDefaults);
  342. return resetButton;
  343. };
  344.  
  345. const createStylesheet = () => {
  346. const stylesheet = document.createElement('style');
  347. stylesheet.innerHTML =
  348. `.mkb-grid {
  349. background-color: #161a22;
  350. height: 300px;
  351. overflow-y: auto;
  352. width: 100%;
  353. }
  354. .mkb-row {
  355. cursor: pointer;
  356. display: flex;
  357. }
  358. .mkb-row:nth-of-type(even) {
  359. background-color: rgba(255, 255, 255, 0.03);
  360. }
  361. .mkb-row:hover {
  362. background-color: rgba(255, 255, 255, 0.1);
  363. }
  364.  
  365. .mkb-row.mkb-listening {
  366. background-color: #577baa !important;
  367. }
  368. .mkb-cell {
  369. align-items: center;
  370. display: flex;
  371. flex: 1 1 auto;
  372. padding: 5px;
  373. width: 100%;
  374. }
  375. .mkb-grid kbd {
  376. background-color: hsl(210, 8%, 90%);
  377. border: 1px solid hsl(210, 8%, 65%);
  378. border-radius: 3px;
  379. box-shadow: 0 1px 1px hsla(210, 8%, 5%, 0.15),
  380. inset 1px 1px 0 #ffffff;
  381. color: hsl(210, 8%, 15%);
  382. font-size: 66%;
  383. margin: 0 5px;
  384. min-width: 26px;
  385. padding: 3px;
  386. text-align: center;
  387. text-shadow: #ffffff;
  388. }
  389. .mkb-grid kbd:first-of-type {
  390. margin-left: 0px;
  391. }
  392. .mkb-grid kbd:last-of-type {
  393. margin-right: 0px;
  394. }`;
  395. return stylesheet;
  396. };
  397. const inject = () => {
  398. const isGameLoaded = window.isLoaded && !window.currentlyCatchingUp;
  399. if (!isGameLoaded) {
  400. setTimeout(inject, 50);
  401. return;
  402. }
  403.  
  404. const grid = createGrid();
  405. keybindings.forEach(k => {
  406. k.row = createRow(k);
  407. grid.appendChild(k.row);
  408. });
  409.  
  410. settingsGrid = grid;
  411.  
  412. const notifications = Array.from(document.querySelectorAll('#settings-container h2')).find(e => e.textContent === 'Notification Settings');
  413. notifications.parentNode.insertBefore(createHeader(), notifications);
  414. notifications.parentNode.insertBefore(createWrapper(grid), notifications);
  415. document.head.appendChild(createStylesheet());
  416. };
  417.  
  418. const beginListeningForRemap = (keybinding) => {
  419. if (keybinding === bindingBeingRemapped) {
  420. endListeningForRemap();
  421. return;
  422. }
  423. endListeningForRemap();
  424. keybinding.row.classList.add('mkb-listening');
  425. bindingBeingRemapped = keybinding;
  426. };
  427.  
  428. const endListeningForRemap = () => {
  429. if (!bindingBeingRemapped) return;
  430. bindingBeingRemapped.row.classList.remove('mkb-listening');
  431. bindingBeingRemapped = null;
  432. };
  433.  
  434. const resetDefaults = () => {
  435. const reset = [];
  436. keybindings.forEach(k => {
  437. const conflict = keybindings.some(kb => reset.includes(kb.name) && parseKeypress(kb.keys) === parseKeypress(k.defaultKeys));
  438. reset.push(k.name);
  439. remap(conflict ? {} : k.defaultKeys, k);
  440. });
  441. };
  442.  
  443. const remap = (keys, keybinding) => {
  444. if (keys.key) {
  445. const conflict = keybindings.find(k => k.name !== keybinding.name && parseKeypress(k.keys) === parseKeypress(keys));
  446. if (conflict) remap({}, conflict);
  447. }
  448. keybinding.keys = keys;
  449. if (settingsGrid !== null) {
  450. const keyCell = createCell(keys);
  451. keybinding.row.replaceChild(keyCell, keybinding.keyCell);
  452. keybinding.keyCell = keyCell;
  453. }
  454.  
  455. saveData();
  456. updateKeymap();
  457. };
  458.  
  459. const updateKeymap = () => {
  460. keymap = {};
  461. keybindings.forEach(k => {
  462. const keypress = parseKeypress(k.keys);
  463. if (keypress) keymap[keypress] = k.callback
  464. });
  465. };
  466.  
  467. const toKeys = (e) => {
  468. if (!e.key) return {};
  469. let key = e.key.toUpperCase();
  470. if (key === 'ESCAPE') key = 'ESC';
  471. else if (key === ' ') key = 'SPACE';
  472. else if (key === '\n') key = 'ENTER';
  473. return { key, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey };
  474. };
  475.  
  476. const parseKeypress = (e) => {
  477. if (!e.key) return '';
  478. if (invalidKeys.includes(e.key)) return '';
  479.  
  480. let keys = [];
  481. if (e.ctrlKey) keys.push('CTRL');
  482. if (e.altKey) keys.push('ALT');
  483. keys.push(e.key.toUpperCase());
  484.  
  485. return keys.join('+');
  486. };
  487.  
  488. const doNotTriggerKeybind = (e) => {
  489. return e.target.tagName == 'INPUT'
  490. || e.target.tagName == 'SELECT'
  491. || e.target.tagName == 'TEXTAREA'
  492. || e.target.isContentEditable;
  493. };
  494.  
  495. const saveData = () => {
  496. const data = keybindings.map(k => ({ bindTo: k.name, keys: k.keys }));
  497. const existingData = getSavedData();
  498. existingData.forEach(d => {
  499. if (!data.some(dt => dt.bindTo === d.bindTo))
  500. data.push(d);
  501. });
  502. localStorage.setItem('MKB-data', JSON.stringify(data));
  503. };
  504.  
  505. const loadData = () => {
  506. const data = getSavedData();
  507. data.forEach(k => {
  508. const match = keybindings.find(kb => kb.name === k.bindTo);
  509. if (match) match.keys = k.keys;
  510. });
  511. keybindings.filter(k => !k.keys).forEach(k => k.keys = k.defaultKeys);
  512. };
  513.  
  514. const getSavedData = () => {
  515. const dataJson = localStorage.getItem('MKB-data');
  516. if (!dataJson) return [];
  517. return JSON.parse(dataJson);
  518. };
  519.  
  520. const onKeyPress = (e) => {
  521. if (doNotTriggerKeybind(e)) return true;
  522. if (e.repeat) return true;
  523. const keysPressed = parseKeypress(toKeys(e));
  524. if (!keysPressed) return true;
  525.  
  526. if (bindingBeingRemapped) {
  527. if (e.key !== 'Escape') {
  528. if (e.ctrlKey && e.altKey && e.key === ' ') remap({}, bindingBeingRemapped);
  529. else remap(toKeys(e), bindingBeingRemapped);
  530. }
  531. endListeningForRemap();
  532. e.preventDefault();
  533. return false;
  534. }
  535.  
  536. if (!keymap[keysPressed]) return true;
  537.  
  538. e.preventDefault();
  539. keymap[keysPressed]();
  540. return false;
  541. };
  542.  
  543. const trackCurrentPage = () => {
  544. if (window.currentPage === undefined) return;
  545. if (currentPage === cachePage) return;
  546. endListeningForRemap();
  547. previousPage = cachePage;
  548. cachePage = currentPage;
  549. };
  550.  
  551. const initialize = () => {
  552. if (window.kb) return;
  553.  
  554. console.log('Initializing Keybindings...');
  555. loadData();
  556. updateKeymap();
  557. document.addEventListener('keydown', onKeyPress);
  558. inject();
  559. setInterval(trackCurrentPage, 10);
  560. console.log('Keybindings initialized.');
  561. };
  562.  
  563. const register = (name, category, defaultKeys, callback) => {
  564. if (typeof callback !== 'function') throw `Expected type of callback is function, instead found ${typeof callback}.`;
  565.  
  566. const conflictingName = keybindings.find(k => k.name === name);
  567. if (conflictingName) throw `A keybinding with the name "${name}" already exists. Please select another name and try again.`;
  568.  
  569. let keys = defaultKeys;
  570. if (defaultKeys && defaultKeys.key) {
  571. const conflictingKeys = keybindings.find(k => parseKeypress(k.keys) === parseKeypress(defaultKeys));
  572. if (conflictingKeys) {
  573. keys = {};
  574. console.warn(`A keybinding matching ${parseKeypress(defaultKeys)} already exists. "${name}" will be unbound by default.`);
  575. }
  576. }
  577.  
  578. const keybinding = { name, category, defaultKeys, keys, callback };
  579. keybindings.push(keybinding);
  580. if (settingsGrid !== null) {
  581. keybinding.row = createRow(keybinding);
  582. settingsGrid.appendChild(keybinding.row);
  583. }
  584. const savedData = getSavedData().find(d => d.bindTo === name);
  585. if (savedData) remap(savedData.keys, keybinding);
  586. updateKeymap();
  587. };
  588.  
  589. initialize();
  590.  
  591. return {
  592. register,
  593. };
  594. })();

QingJ © 2025

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