您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds keybindings to Melvor Idle. Visit the Settings menu (X) to view all keybinds.
// ==UserScript== // @name Keybindings // @description Adds keybindings to Melvor Idle. Visit the Settings menu (X) to view all keybinds. // @version 1.1.1 // @license MIT // @match https://*.melvoridle.com/* // @exclude https://wiki.melvoridle.com* // @grant none // @namespace https://github.com/ChaseStrackbein/melvor-keybindings // ==/UserScript== window.kb = (() => { const invalidKeys = ['SHIFT', 'CONTROL', 'ALT', 'META']; let cachePage = window.currentPage; let previousPage = -1; let settingsGrid = null; let bindingBeingRemapped = null; let keymap = {}; const keybindings = [ { name: 'Menu', category: 'General', defaultKeys: { key: 'M' }, callback: () => document.getElementById('page-header-user-dropdown').click() }, { name: 'Save', category: 'General', defaultKeys: { key: 'S', ctrlKey: true }, callback: () => forceSync(false, false) }, { name: 'Reload / Character Select', category: 'General', defaultKeys: { key: 'F5' }, callback: () => window.location.reload() }, { name: 'Open Wiki', category: 'General', defaultKeys: { key: 'F1' }, callback: () => window.open('https://wiki.melvoridle.com/', '_blank') }, { name: 'Settings', category: 'General', defaultKeys: { key: 'X' }, callback: () => changePage(CONSTANTS.page.Settings, false, false) }, { name: 'Shop', category: 'General', defaultKeys: { key: 'V' }, callback: () => changePage(CONSTANTS.page.Shop, false, false) }, { name: 'Bank', category: 'General', defaultKeys: { key: 'B' }, callback: () => changePage(CONSTANTS.page.Bank, false, false) }, { name: 'Combat', category: 'General', defaultKeys: { key: 'C' }, callback: () => changePage(CONSTANTS.page.Combat, false, false) }, { name: 'Eat Equipped Food', category: 'General', defaultKeys: { key: 'H' }, callback: () => player.eatFood() }, { name: 'Loot All (Combat)', category: 'General', defaultKeys: { key: 'SPACE' }, callback: () => combatManager.loot.lootAll() }, { name: 'Run (Combat)', category: 'General', defaultKeys: { key: 'SPACE', ctrlKey: true }, callback: () => combatManager.stopCombat() }, { name: 'Equipment Set 1', category: 'General', defaultKeys: { key: '1', ctrlKey: true }, callback: () => player.changeEquipmentSet(0) }, { name: 'Equipment Set 2', category: 'General', defaultKeys: { key: '2', ctrlKey: true }, callback: () => player.changeEquipmentSet(1) }, { name: 'Equipment Set 3', category: 'General', defaultKeys: { key: '3', ctrlKey: true }, callback: () => player.changeEquipmentSet(2) }, { name: 'Equipment Set 4', category: 'General', defaultKeys: { key: '4', ctrlKey: true }, callback: () => player.changeEquipmentSet(3) }, { name: 'Search Bank', category: 'General', defaultKeys: { key: 'F', ctrlKey: true }, callback: () => { changePage(CONSTANTS.page.Bank, false, false); updateBankSearchArray(); document.getElementById('searchTextbox').focus(); } }, { name: 'Summoning Synergies Menu', category: 'General', defaultKeys: { key: 'S' }, callback: () => { const modal = $('#modal-summoning-synergy').data('bs.modal'); if (!modal || !modal._isShown) openSynergiesBreakdown(); else modal.hide(); } }, { name: 'Search Summoning Synergies', category: 'General', defaultKeys: { key: 'F', ctrlKey: true, altKey: true }, callback: () => { openSynergiesBreakdown(); document.getElementById('summoning-synergy-search').focus(); } }, { name: 'Woodcutting', category: 'General', defaultKeys: { key: '1' }, callback: () => changePage(CONSTANTS.page.Woodcutting, false, false) }, { name: 'Fishing', category: 'General', defaultKeys: { key: '2' }, callback: () => changePage(CONSTANTS.page.Fishing, false, false) }, { name: 'Firemaking', category: 'General', defaultKeys: { key: '3' }, callback: () => changePage(CONSTANTS.page.Firemaking, false, false) }, { name: 'Cooking', category: 'General', defaultKeys: { key: '4' }, callback: () => changePage(CONSTANTS.page.Cooking, false, false) }, { name: 'Mining', category: 'General', defaultKeys: { key: '5' }, callback: () => changePage(CONSTANTS.page.Mining, false, false) }, { name: 'Smithing', category: 'General', defaultKeys: { key: '6' }, callback: () => changePage(CONSTANTS.page.Smithing, false, false) }, { name: 'Thieving', category: 'General', defaultKeys: { key: '7' }, callback: () => changePage(CONSTANTS.page.Thieving, false, false) }, { name: 'Farming', category: 'General', defaultKeys: { key: '8' }, callback: () => changePage(CONSTANTS.page.Farming, false, false) }, { name: 'Fletching', category: 'General', defaultKeys: { key: '9' }, callback: () => changePage(CONSTANTS.page.Fletching, false, false) }, { name: 'Crafting', category: 'General', defaultKeys: { key: '0' }, callback: () => changePage(CONSTANTS.page.Crafting, false, false) }, { name: 'Runecrafting', category: 'General', defaultKeys: { key: '!' }, callback: () => changePage(CONSTANTS.page.Runecrafting, false, false) }, { name: 'Herblore', category: 'General', defaultKeys: { key: '@' }, callback: () => changePage(CONSTANTS.page.Herblore, false, false) }, { name: 'Agility', category: 'General', defaultKeys: { key: '#' }, callback: () => changePage(CONSTANTS.page.Agility, false, false) }, { name: 'Summoning', category: 'General', defaultKeys: { key: '$' }, callback: () => changePage(CONSTANTS.page.Summoning, false, false) }, { name: 'Astrology', category: 'General', defaultKeys: { key: '%' }, callback: () => changePage(CONSTANTS.page.Astrology, false, false) }, { name: 'Alt. Magic', category: 'General', defaultKeys: { key: 'M', altKey: true }, callback: () => changePage(CONSTANTS.page.AltMagic, false, false) }, { name: 'Completion Log', category: 'General', defaultKeys: { key: 'Y' }, callback: () => changePage(30, false, false) }, { name: 'Statistics', category: 'General', defaultKeys: { key: 'F2' }, callback: () => changePage(CONSTANTS.page.Statistics, false, false) }, { name: 'Golbin Raid', category: 'General', defaultKeys: { key: 'G' }, callback: () => changePage(CONSTANTS.page.GolbinRaid, false, false) }, { name: 'Previous Page', category: 'General', defaultKeys: { key: 'BACKSPACE' }, callback: () => changePage(previousPage, false, false) } ]; const createHeader = () => { const header = document.createElement('h2'); header.classList.add('content-heading', 'border-bottom', 'mb-4', 'pb-2'); header.innerHTML = 'Keybindings'; return header; }; const createHelpText = () => { const helpText = document.createElement('div'); helpText.classList.add('font-size-sm', 'text-muted', 'ml-2', 'mb-2'); 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.'; return helpText; }; const createWrapper = (grid) => { const row = document.createElement('div'); row.classList.add('row'); const column = document.createElement('div'); column.classList.add('col-md-6', 'offset-md-3'); const wrapper = document.createElement('div'); wrapper.classList.add('mb-4'); wrapper.appendChild(createHelpText()); wrapper.appendChild(grid); wrapper.appendChild(createResetButton()); column.appendChild(wrapper); row.appendChild(column); return row; }; const createGrid = () => { const grid = document.createElement('div'); grid.classList.add('mkb-grid'); return grid; }; const createRow = (keybinding) => { const row = document.createElement('div'); row.classList.add('mkb-row'); row.addEventListener('click', () => beginListeningForRemap(keybinding)); const nameCell = createCell(keybinding.name); const keyCell = createCell(keybinding.keys); row.appendChild(nameCell); row.appendChild(keyCell); keybinding.keyCell = keyCell; return row; }; const createCell = (keysOrText) => { const cell = document.createElement('div'); cell.classList.add('mkb-cell'); if (typeof keysOrText === 'string') cell.innerHTML = keysOrText; else { if (keysOrText.ctrlKey) { cell.appendChild(createKbd('CTRL')); cell.appendChild(createPlus()); } if (keysOrText.altKey) { cell.appendChild(createKbd('ALT')); cell.appendChild(createPlus()); } if (keysOrText.key) cell.appendChild(createKbd(keysOrText.key)); } return cell; }; const createKbd = (text) => { const kbd = document.createElement('kbd'); kbd.innerHTML = text; return kbd; }; const createPlus = () => { const plus = document.createTextNode('+'); return plus; }; const createResetButton = () => { const resetButton = document.createElement('button'); resetButton.type = 'button'; resetButton.classList.add('btn', 'btn-sm', 'btn-danger', 'm-1'); resetButton.innerHTML = 'Reset Default Keybindings'; resetButton.addEventListener('click', resetDefaults); return resetButton; }; const createStylesheet = () => { const stylesheet = document.createElement('style'); stylesheet.innerHTML = `.mkb-grid { background-color: #161a22; height: 300px; overflow-y: auto; width: 100%; } .mkb-row { cursor: pointer; display: flex; } .mkb-row:nth-of-type(even) { background-color: rgba(255, 255, 255, 0.03); } .mkb-row:hover { background-color: rgba(255, 255, 255, 0.1); } .mkb-row.mkb-listening { background-color: #577baa !important; } .mkb-cell { align-items: center; display: flex; flex: 1 1 auto; padding: 5px; width: 100%; } .mkb-grid kbd { background-color: hsl(210, 8%, 90%); border: 1px solid hsl(210, 8%, 65%); border-radius: 3px; box-shadow: 0 1px 1px hsla(210, 8%, 5%, 0.15), inset 1px 1px 0 #ffffff; color: hsl(210, 8%, 15%); font-size: 66%; margin: 0 5px; min-width: 26px; padding: 3px; text-align: center; text-shadow: #ffffff; } .mkb-grid kbd:first-of-type { margin-left: 0px; } .mkb-grid kbd:last-of-type { margin-right: 0px; }`; return stylesheet; }; const inject = () => { const isGameLoaded = window.isLoaded && !window.currentlyCatchingUp; if (!isGameLoaded) { setTimeout(inject, 50); return; } const grid = createGrid(); keybindings.forEach(k => { k.row = createRow(k); grid.appendChild(k.row); }); settingsGrid = grid; const notifications = Array.from(document.querySelectorAll('#settings-container h2')).find(e => e.textContent === 'Notification Settings'); notifications.parentNode.insertBefore(createHeader(), notifications); notifications.parentNode.insertBefore(createWrapper(grid), notifications); document.head.appendChild(createStylesheet()); }; const beginListeningForRemap = (keybinding) => { if (keybinding === bindingBeingRemapped) { endListeningForRemap(); return; } endListeningForRemap(); keybinding.row.classList.add('mkb-listening'); bindingBeingRemapped = keybinding; }; const endListeningForRemap = () => { if (!bindingBeingRemapped) return; bindingBeingRemapped.row.classList.remove('mkb-listening'); bindingBeingRemapped = null; }; const resetDefaults = () => { const reset = []; keybindings.forEach(k => { const conflict = keybindings.some(kb => reset.includes(kb.name) && parseKeypress(kb.keys) === parseKeypress(k.defaultKeys)); reset.push(k.name); remap(conflict ? {} : k.defaultKeys, k); }); }; const remap = (keys, keybinding) => { if (keys.key) { const conflict = keybindings.find(k => k.name !== keybinding.name && parseKeypress(k.keys) === parseKeypress(keys)); if (conflict) remap({}, conflict); } keybinding.keys = keys; if (settingsGrid !== null) { const keyCell = createCell(keys); keybinding.row.replaceChild(keyCell, keybinding.keyCell); keybinding.keyCell = keyCell; } saveData(); updateKeymap(); }; const updateKeymap = () => { keymap = {}; keybindings.forEach(k => { const keypress = parseKeypress(k.keys); if (keypress) keymap[keypress] = k.callback }); }; const toKeys = (e) => { if (!e.key) return {}; let key = e.key.toUpperCase(); if (key === 'ESCAPE') key = 'ESC'; else if (key === ' ') key = 'SPACE'; else if (key === '\n') key = 'ENTER'; return { key, ctrlKey: e.ctrlKey, altKey: e.altKey, metaKey: e.metaKey }; }; const parseKeypress = (e) => { if (!e.key) return ''; if (invalidKeys.includes(e.key)) return ''; let keys = []; if (e.ctrlKey) keys.push('CTRL'); if (e.altKey) keys.push('ALT'); keys.push(e.key.toUpperCase()); return keys.join('+'); }; const doNotTriggerKeybind = (e) => { return e.target.tagName == 'INPUT' || e.target.tagName == 'SELECT' || e.target.tagName == 'TEXTAREA' || e.target.isContentEditable; }; const saveData = () => { const data = keybindings.map(k => ({ bindTo: k.name, keys: k.keys })); const existingData = getSavedData(); existingData.forEach(d => { if (!data.some(dt => dt.bindTo === d.bindTo)) data.push(d); }); localStorage.setItem('MKB-data', JSON.stringify(data)); }; const loadData = () => { const data = getSavedData(); data.forEach(k => { const match = keybindings.find(kb => kb.name === k.bindTo); if (match) match.keys = k.keys; }); keybindings.filter(k => !k.keys).forEach(k => k.keys = k.defaultKeys); }; const getSavedData = () => { const dataJson = localStorage.getItem('MKB-data'); if (!dataJson) return []; return JSON.parse(dataJson); }; const onKeyPress = (e) => { if (doNotTriggerKeybind(e)) return true; if (e.repeat) return true; const keysPressed = parseKeypress(toKeys(e)); if (!keysPressed) return true; if (bindingBeingRemapped) { if (e.key !== 'Escape') { if (e.ctrlKey && e.altKey && e.key === ' ') remap({}, bindingBeingRemapped); else remap(toKeys(e), bindingBeingRemapped); } endListeningForRemap(); e.preventDefault(); return false; } if (!keymap[keysPressed]) return true; e.preventDefault(); keymap[keysPressed](); return false; }; const trackCurrentPage = () => { if (window.currentPage === undefined) return; if (currentPage === cachePage) return; endListeningForRemap(); previousPage = cachePage; cachePage = currentPage; }; const initialize = () => { if (window.kb) return; console.log('Initializing Keybindings...'); loadData(); updateKeymap(); document.addEventListener('keydown', onKeyPress); inject(); setInterval(trackCurrentPage, 10); console.log('Keybindings initialized.'); }; const register = (name, category, defaultKeys, callback) => { if (typeof callback !== 'function') throw `Expected type of callback is function, instead found ${typeof callback}.`; const conflictingName = keybindings.find(k => k.name === name); if (conflictingName) throw `A keybinding with the name "${name}" already exists. Please select another name and try again.`; let keys = defaultKeys; if (defaultKeys && defaultKeys.key) { const conflictingKeys = keybindings.find(k => parseKeypress(k.keys) === parseKeypress(defaultKeys)); if (conflictingKeys) { keys = {}; console.warn(`A keybinding matching ${parseKeypress(defaultKeys)} already exists. "${name}" will be unbound by default.`); } } const keybinding = { name, category, defaultKeys, keys, callback }; keybindings.push(keybinding); if (settingsGrid !== null) { keybinding.row = createRow(keybinding); settingsGrid.appendChild(keybinding.row); } const savedData = getSavedData().find(d => d.bindTo === name); if (savedData) remap(savedData.keys, keybinding); updateKeymap(); }; initialize(); return { register, }; })();