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