Keybindings

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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