Melvor Combat Calculator

Calculates how many levels you need to reach the next combat level!

// ==UserScript==
// @name         Melvor Combat Calculator
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  Calculates how many levels you need to reach the next combat level!
// @author       martijnd
// @match		 https://*.melvoridle.com/*
// @exclude		 https://wiki.melvoridle.com*
// @noframes
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  const COMBAT_SKILLS = [
    CONSTANTS.skill.Attack,
    CONSTANTS.skill.Strength,
    CONSTANTS.skill.Defence,
    CONSTANTS.skill.Hitpoints,
    CONSTANTS.skill.Ranged,
    CONSTANTS.skill.Magic,
    CONSTANTS.skill.Prayer,
  ];

  const interval = setInterval(() => {
    const element = document.getElementById("nav-skill-tooltip-69");
    if (element) {
      element.addEventListener("mouseenter", () => {
        COMBAT_SKILLS.forEach((skillId) => {
          appendLvlEl(skillId);
        });
      });

      element.addEventListener("mouseleave", () => {
        COMBAT_SKILLS.forEach((skillId) => {
          removeLvlEl(skillId);
        });
      });

      clearInterval(interval);
    }
  }, 1000);
})();

const lvlToXp = Array.from({ length: 200 }, (_, i) => exp.level_to_xp(i));

function appendLvlEl(skillId) {
  const span = document.createElement("span");
  span.innerText = getLevelsNeeded(skillId);
  span.id = `skill-togo-${skillId}`;
  span.style.position = "absolute";
  span.style.top = "4px";
  span.style.left = "18px";
  span.style.color = "#fff";
  span.style.textAlign = "center";
  span.style.width = "30px";
  span.style.height = "30px";
  span.style.borderRadius = "5px";
  span.style.padding = "3px";
  span.style.backgroundColor = "#5cace5";
  span.style.border = "3px solid #5cace5";

  document.getElementById(`skill-nav-name-${skillId}`).append(span);
}

function removeLvlEl(skillId) {
  const id = `skill-togo-${skillId}`;
  const el = document.getElementById(id);
  if (el) {
    el.remove();
  }
}

function calculateCombatLevel(skillId = null, level = null) {
  function getLevel(id) {
    return skillId === id ? level : convertXpToLvl(skillXP[id]);
  }

  const attackLevel = getLevel(CONSTANTS.skill.Attack);
  const strengthLevel = getLevel(CONSTANTS.skill.Strength);
  const defenceLevel = getLevel(CONSTANTS.skill.Defence);
  const prayerLevel = getLevel(CONSTANTS.skill.Prayer);
  const hitpointsLevel = getLevel(CONSTANTS.skill.Hitpoints);
  const rangedLevel = getLevel(CONSTANTS.skill.Ranged);
  const magicLevel = getLevel(CONSTANTS.skill.Magic);

  const baseCombatLevel =
    0.25 * (defenceLevel + hitpointsLevel + prayerLevel / 2);
  const meleeCombatLevel = 0.325 * (attackLevel + strengthLevel);
  const rangedCombatLevel = 0.325 * ((3 * rangedLevel) / 2);
  const magicCombatLevel = 0.325 * ((3 * magicLevel) / 2);

  return Math.floor(
    baseCombatLevel +
      Math.max(meleeCombatLevel, rangedCombatLevel, magicCombatLevel)
  );
}

function getLevelsNeeded(skillId) {
  const nextCombatLevel = calculateCombatLevel() + 1;

  for (let i = 0; i < 99; i++) {
    const combatLevel = calculateCombatLevel(
      skillId,
      convertXpToLvl(skillXP[skillId]) + i
    );
    if (combatLevel === nextCombatLevel) {
      return i;
    }
  }
}

/**
 * Thanks to Melvor ETA for this function
 * @see https://github.com/gmiclotte/Melvor-ETA
 */
function binarySearch(array, pred) {
  let lo = -1,
    hi = array.length;
  while (1 + lo < hi) {
    const mi = lo + ((hi - lo) >> 1);
    if (pred(array[mi])) {
      hi = mi;
    } else {
      lo = mi;
    }
  }
  return hi;
}

/**
 * Thanks to Melvor ETA for this function
 * @see https://github.com/gmiclotte/Melvor-ETA
 */
function convertXpToLvl(xp, noCap = false) {
  let level = binarySearch(lvlToXp, (t) => xp <= t) - 1;
  if (level < 1) {
    level = 1;
  } else if (!noCap && level > 99) {
    level = 99;
  }
  return level;
}