Melvor Auto Miner

Helpful tool for auto-mining in Melvor

// ==UserScript==
// @name         Melvor Auto Miner
// @namespace    https://github.com/shamus03
// @version      1.6
// @description  Helpful tool for auto-mining in Melvor
// @author       Shamus03
// @match        https://*.melvoridle.com/*
// @exclude      https://wiki.melvoridle.com/*
// @grant        none
// ==/UserScript==

(function() {
  'use strict'

  const bus = document.createElement('div')

  const getItemQuantityInBank = itemId => {
    let qty = 0
    for (const item of bank) {
      if (item.id === itemId) {
        qty += item.qty
      }
    }
    return qty
  }

  const tryTriggerSemiSkillChanged = () => {
    SEMIEventBus && typeof SEMIEventBus.TriggerSkillChanged === 'function' && SEMIEventBus.TriggerSkillChanged()
  }

  const trySelectOre = ore => {
    if (rockData[ore].depleted) {
      return false
    }
    if (ore !== currentRock) {
      setTimeout(() => {
        mineRock(ore, true)
        tryTriggerSemiSkillChanged()
      })
    }
    return true
  }

  const ORES = {
    Rune_Essence: 10,
    Copper: 0,
    Tin: 1,
    Iron: 2,
    Coal: 3,
    Silver: 4,
    Gold: 5,
    Mithril: 6,
    Adamantite: 7,
    Runite: 8,
    Dragonite: 9,
  }

  const ORE_ITEM_IDS = {
    [ORES.Rune_Essence]: CONSTANTS.item.Rune_Essence,
    [ORES.Copper]: CONSTANTS.item.Copper_Ore,
    [ORES.Tin]: CONSTANTS.item.Tin_Ore,
    [ORES.Iron]: CONSTANTS.item.Iron_Ore,
    [ORES.Coal]: CONSTANTS.item.Coal_Ore,
    [ORES.Silver]: CONSTANTS.item.Silver_Ore,
    [ORES.Gold]: CONSTANTS.item.Gold_Ore,
    [ORES.Mithril]: CONSTANTS.item.Mithril_Ore,
    [ORES.Adamantite]: CONSTANTS.item.Adamantite_Ore,
    [ORES.Runite]: CONSTANTS.item.Runite_Ore,
    [ORES.Dragonite]: CONSTANTS.item.Dragonite_Ore,
  }

  const BAR_RATIOS = {}
  for (const item of items) {
    if (item.type !== 'Bar') continue

    const ratios = {}
    for (const reqItem of item.smithReq) {
      ratios[items[reqItem.id].miningID] = reqItem.qty
    }
    BAR_RATIOS[item.id] = ratios
  }

  const ADDITIONAL_OPTIONS = {
    None: 'None',
    Lowest_Mastery: 'Lowest_Mastery',
    Highest_Ore_Not_Maxed: 'Highest_Ore_Not_Maxed',
  }

  const localStorageKey = 'mep-auto-mine-last-selected-bar'

  let selectedBar = localStorage.getItem(localStorageKey)
  if (selectedBar === '') {
    selectedBar = ADDITIONAL_OPTIONS.None
  }

  const trySetupBarSelection = () => {
    const container = document.getElementById('mining-ores-container')
    if (!container) return false

    const spacer = document.createElement('div')
    spacer.classList.add('col-12')

    const col = document.createElement('div')
    col.classList.add('col-auto')

    const lbl = document.createElement('label')
    lbl.classList.add('form-group', 'form-inline', 'block', 'block-content', 'block-content-full', 'border-4x', 'border-mining', 'border-top')
    lbl.innerText = 'Auto-Mining:'
    lbl.innerH
    col.append(lbl)

    const sel = document.createElement('select')
    sel.classList.add('form-control', 'ml-2')
    lbl.append(sel)
    sel.addEventListener('change', () => {
      selectedBar = sel.value
      localStorage.setItem(localStorageKey, selectedBar)
    })

    const addOption = (text, value) => {
      const opt = document.createElement('option')
      opt.innerText = text
      opt.value = value
      sel.append(opt)
    }

    addOption('None', ADDITIONAL_OPTIONS.None)
    for (const itemId in BAR_RATIOS) {
      const item = items[itemId]
      addOption(item.name, itemId)
    }
    for (const k in ADDITIONAL_OPTIONS) {
      if (k === ADDITIONAL_OPTIONS.None) continue
      const name = k.replace(/_/g, ' ')
      addOption(name, k)
    }
    sel.value = selectedBar

    container.prepend(spacer)
    container.prepend(col)
    return true
  }

  (async function() {
    while (!trySetupBarSelection()) {
      await new Promise(resolve => setTimeout(resolve, 1000))
    }
  })()

  const getOreMasteryLevel = ore => {
    return MASTERY[CONSTANTS.skill.Mining].xp[ore]
  }

  const getOrePriority = () => {
    switch (selectedBar) {
      case ADDITIONAL_OPTIONS.Lowest_Mastery:
        const levels = {}
        for (const ore of Object.values(ORES)) {
          levels[ore] = getOreMasteryLevel(ore)
        }
        return Object.values(ORES).sort((a, b) => levels[a] - levels[b])
      case ADDITIONAL_OPTIONS.Highest_Ore_Not_Maxed:
        const prios = {}
        for (const ore of Object.values(ORES).reverse()) {
          prios[ore] = getOreMasteryLevel(ore) < 13034431 ? 1 : 0
        }
        return Object.values(ORES).reverse().sort((a, b) => prios[b] - prios[a])
    }

    const selectedRatios = BAR_RATIOS[selectedBar] || {}

    const currentRatios = {}
    for (const ore of Object.values(ORES)) {
      const qty = getItemQuantityInBank(ORE_ITEM_IDS[ore])
      const needRatio = selectedRatios[ore] || 0

      currentRatios[ore] = (needRatio > 0)
        ? qty === 0 ? 0 : qty / needRatio
      : Infinity
    }

    return Object.values(ORES).sort((a, b) => currentRatios[a] - currentRatios[b])
  }

  const selectBestOre = () => {
    const orePriority = getOrePriority()

    for (const ore of orePriority) {
      if (trySelectOre(ore)) {
        return
      }
    }
  }

  const utils = {
    selectBestOre,
    getItemQuantityInBank,
    getOrePriority,
  }

  const events = {
    rockDamaged: 'rock-damaged',
    rockRespawned: 'rock-respawned',
    glovesChargesDecreased: 'gloves-charges-decreased',
  }

  const originalFunctions = {}
  const wrap = (name, handler) => {
    originalFunctions[name] = window[name]
    window[name] = function(...args) {
      const result = originalFunctions[name](...args)
      try {
        handler(...args)
      } catch(ex) {
        console.error(ex)
      }
      return result
    }
  }

  wrap('updateRockHP', (ore) => {
    const { depleted, damage } = rockData[ore]
    if (damage === 0) {
      bus.dispatchEvent(new CustomEvent(events.rockRespawned, {detail:{ore}}))
    } else {
      bus.dispatchEvent(new CustomEvent(events.rockDamaged, {detail:{ore,depleted,damage}}))
    }
  })

  wrap('updateGloves', (gloves, skill) => {
    const { isActive, remainingActions } = glovesTracker[gloves]
    bus.dispatchEvent(new CustomEvent(events.glovesChargesDecreased, {detail:{gloves,skill,isActive,remainingActions}}))
  })

  bus.addEventListener(events.rockDamaged, e => {
    if (isMining && selectedBar !== ADDITIONAL_OPTIONS.None) {
      selectBestOre()
    }
  })

  const getEquippedGloves = () => {
    return combatManager.player.equipmentSets[combatManager.player.selectedEquipmentSet].slots.Gloves.item
  }

  bus.addEventListener(events.glovesChargesDecreased, ({ detail: { gloves, skill, isActive, remainingActions }}) => {
    const equippedGloves = getEquippedGloves()
    if (!isActive || remainingActions > 1 || !equippedGloves || equippedGloves.gloveID !== gloves) return

    const haveEnoughGp = gp >= glovesCost[gloves]

    switch (skill) {
      case CONSTANTS.skill.Mining:
        if (!isMining) return
        break
      case CONSTANTS.skill.Thieving:
        if (!isThieving) return
        break
      default:
        return
    }

    const oldBuyQty = buyQty
    buyQty = 1
    buyShopItem('Gloves', gloves, true)
    buyQty = oldBuyQty
  })

  window.mep = {
    bus,
    events,
    utils,
    originalFunctions,
  }
})()